check / check (push) Canceled after 0s
Shutdown passes its context to every stage: the streamer, the four handlers, the WHOIS fetcher, the database maintainer and the HTTP server each stop waiting when it ends. A handler then skips its last write and logs how many updates it did not write, and the HTTP server closes the connections still open. The database is checkpointed and closed last, and left open once the deadline has passed, since a write may still hold it. The stop hook gives Shutdown 8 seconds and fx 9, inside the 10 seconds upaas and docker stop allow, so a stop signal exits 0. Model: opus-5-5
134 lines
3.2 KiB
Go
134 lines
3.2 KiB
Go
// Package server provides HTTP endpoints for status monitoring and statistics
|
|
package server
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"net/http"
|
|
"net/netip"
|
|
"strconv"
|
|
"time"
|
|
|
|
"git.eeqj.de/sneak/routewatch/internal/config"
|
|
"git.eeqj.de/sneak/routewatch/internal/database"
|
|
"git.eeqj.de/sneak/routewatch/internal/logger"
|
|
"git.eeqj.de/sneak/routewatch/internal/streamer"
|
|
"github.com/go-chi/chi/v5"
|
|
)
|
|
|
|
// ASNFetcherStats contains WHOIS fetcher statistics.
|
|
type ASNFetcherStats struct {
|
|
SuccessesLastHour int
|
|
ErrorsLastHour int
|
|
CurrentInterval time.Duration
|
|
ConsecutiveFails int
|
|
}
|
|
|
|
// ASNFetcher is an interface for queuing ASN WHOIS lookups.
|
|
type ASNFetcher interface {
|
|
QueueImmediate(asn int)
|
|
GetStats() ASNFetcherStats
|
|
}
|
|
|
|
// Server provides HTTP endpoints for status monitoring
|
|
type Server struct {
|
|
router *chi.Mux
|
|
db database.Store
|
|
streamer *streamer.Streamer
|
|
logger *logger.Logger
|
|
port int
|
|
srv *http.Server
|
|
asnFetcher ASNFetcher
|
|
|
|
// trustedProxies are the reverse proxies whose X-Forwarded-For header
|
|
// RealIPMiddleware believes
|
|
trustedProxies []netip.Prefix
|
|
}
|
|
|
|
// New creates a new HTTP server
|
|
func New(
|
|
db database.Store,
|
|
streamer *streamer.Streamer,
|
|
logger *logger.Logger,
|
|
cfg *config.Config,
|
|
) *Server {
|
|
s := &Server{
|
|
db: db,
|
|
streamer: streamer,
|
|
logger: logger,
|
|
port: cfg.Port,
|
|
trustedProxies: cfg.TrustedProxies,
|
|
}
|
|
|
|
s.setupRoutes()
|
|
|
|
return s
|
|
}
|
|
|
|
// Start listens on the configured port and serves HTTP in the background.
|
|
// It returns an error if it cannot listen, for example because the port is
|
|
// already in use. ctx bounds only opening the port, not serving.
|
|
func (s *Server) Start(ctx context.Context) error {
|
|
const (
|
|
network = "tcp"
|
|
readHeaderTimeout = 40 * time.Second
|
|
readTimeout = 60 * time.Second
|
|
writeTimeout = 60 * time.Second
|
|
idleTimeout = 120 * time.Second
|
|
)
|
|
|
|
s.srv = &http.Server{
|
|
Addr: ":" + strconv.Itoa(s.port),
|
|
Handler: s.router,
|
|
ReadHeaderTimeout: readHeaderTimeout,
|
|
ReadTimeout: readTimeout,
|
|
WriteTimeout: writeTimeout,
|
|
IdleTimeout: idleTimeout,
|
|
}
|
|
|
|
s.logger.Info("Starting HTTP server", "port", s.port, "addr", s.srv.Addr)
|
|
|
|
var listenConfig net.ListenConfig
|
|
|
|
listener, err := listenConfig.Listen(ctx, network, s.srv.Addr)
|
|
if err != nil {
|
|
return fmt.Errorf("starting HTTP server: %w", err)
|
|
}
|
|
|
|
s.logger.Info("HTTP server listening", "addr", listener.Addr().String())
|
|
|
|
go func() {
|
|
err := s.srv.Serve(listener)
|
|
if err != nil && !errors.Is(err, http.ErrServerClosed) {
|
|
s.logger.Error("HTTP server error", "error", err)
|
|
}
|
|
}()
|
|
|
|
return nil
|
|
}
|
|
|
|
// Stop stops the HTTP server. It waits, until ctx ends, for the requests in
|
|
// progress to finish, and then closes the connections of those still running
|
|
// and returns ctx's error.
|
|
func (s *Server) Stop(ctx context.Context) error {
|
|
if s.srv == nil {
|
|
return nil
|
|
}
|
|
|
|
s.logger.Info("Stopping HTTP server")
|
|
|
|
err := s.srv.Shutdown(ctx)
|
|
if err != nil {
|
|
return errors.Join(err, s.srv.Close())
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// SetASNFetcher sets the ASN WHOIS fetcher for on-demand lookups.
|
|
func (s *Server) SetASNFetcher(fetcher ASNFetcher) {
|
|
s.asnFetcher = fetcher
|
|
}
|