repo-sync/server_cmd.go
James Coleman 14cf482549
Some checks are pending
Go package / build (push) Waiting to run
first commit
2026-07-27 14:16:32 -05:00

164 lines
4.6 KiB
Go

package main
import (
"context"
"net"
"net/http"
"os"
"os/signal"
"syscall"
"time"
cfg "github.com/grmrgecko/repo-sync/config"
"github.com/grmrgecko/repo-sync/fetch"
"github.com/grmrgecko/repo-sync/server"
"github.com/grmrgecko/repo-sync/state"
log "github.com/sirupsen/logrus"
)
// shutdownTimeout bounds the graceful HTTP shutdown at exit.
const shutdownTimeout = 30 * time.Second
// ServerCmd runs the caching mirror server.
type ServerCmd struct {
ConfigPath string `help:"The path to the config file." optional:"" type:"existingfile"`
Bind string `name:"http.bind" help:"Override the configured bind address." optional:""`
Port uint `name:"http.port" help:"Override the configured port." optional:""`
}
// updateCFG applies command line overrides onto a freshly loaded
// configuration, before it is installed and visible to serving goroutines.
func (c *ServerCmd) updateCFG(conf *cfg.Config) {
if c.Bind != "" {
conf.HTTP.BindAddr = c.Bind
}
if c.Port != 0 {
conf.HTTP.Port = c.Port
}
}
// newHTTPServer builds the listener configuration from the active config.
func newHTTPServer() *http.Server {
conf := cfg.C.Load()
return &http.Server{
Addr: conf.HTTP.ListenAddr(),
Handler: server.Handler(),
ReadHeaderTimeout: conf.HTTP.ReadHeaderTimeout,
ReadTimeout: conf.HTTP.ReadTimeout,
WriteTimeout: conf.HTTP.WriteTimeout,
IdleTimeout: conf.HTTP.IdleTimeout,
}
}
// startHTTP binds the listener and serves in the background, returning the
// server for a later shutdown.
func startHTTP() (*http.Server, error) {
server := newHTTPServer()
ln, err := net.Listen("tcp", server.Addr)
if err != nil {
return nil, err
}
log.WithField("addr", server.Addr).Info("Mirror server listening.")
go func() {
if err := server.Serve(ln); err != nil && err != http.ErrServerClosed {
log.WithError(err).Error("HTTP server failed.")
}
}()
return server, nil
}
// stopHTTP gracefully shuts a server down within the shutdown timeout.
func stopHTTP(server *http.Server) {
ctx, cancel := context.WithTimeout(context.Background(), shutdownTimeout)
defer cancel()
_ = server.Shutdown(ctx)
}
// Run starts the mirror server: configuration, state, crawl loops, and the
// HTTP listener, then handles signals until shutdown.
func (c *ServerCmd) Run() error {
// Load configuration and apply command line overrides before install.
conf, err := cfg.Load(c.ConfigPath)
if err != nil {
return err
}
c.updateCFG(conf)
cfg.C.Store(conf)
cfg.SetupLogging(conf.Log)
if conf.Crawler.UserAgent != "" {
fetch.SetUserAgent(conf.Crawler.UserAgent)
}
fetch.SetRequestTimeout(conf.Crawler.RequestTimeout)
// Load persisted crawl state and build the shared HTTP client.
if err := state.Load(); err != nil {
return err
}
fetch.Reload()
// Start the background loops and the listener.
loopCtx, loopCancel := context.WithCancel(context.Background())
defer loopCancel()
go state.S.FlushLoop(loopCtx)
crawlDone := make(chan struct{})
go func() {
defer close(crawlDone)
server.CrawlLoop(loopCtx)
}()
// stopCrawls cancels the loops and waits for in-flight crawls so their
// outcomes are recorded before the final state flush. The crawl loop
// must return first: a scheduler pass in flight can still dispatch
// crawls, and a WaitGroup Add racing Wait is a misuse panic.
stopCrawls := func() {
loopCancel()
<-crawlDone
server.WaitCrawls()
}
httpServer, err := startHTTP()
if err != nil {
return err
}
// Handle signals: reload on SIGHUP, shut down on SIGINT/SIGTERM.
signals := make(chan os.Signal, 1)
signal.Notify(signals, os.Interrupt, syscall.SIGTERM, syscall.SIGHUP)
for sig := range signals {
if sig != syscall.SIGHUP {
break
}
log.Info("Reloading configuration.")
oldAddr := cfg.C.Load().HTTP.ListenAddr()
conf, err := cfg.Load(c.ConfigPath)
if err != nil {
log.WithError(err).Error("Unable to reload configuration; keeping previous values.")
continue
}
c.updateCFG(conf)
cfg.C.Store(conf)
cfg.SetupLogging(conf.Log)
if conf.Crawler.UserAgent != "" {
fetch.SetUserAgent(conf.Crawler.UserAgent)
}
fetch.SetRequestTimeout(conf.Crawler.RequestTimeout)
fetch.Reload()
if conf.HTTP.ListenAddr() != oldAddr {
stopHTTP(httpServer)
if httpServer, err = startHTTP(); err != nil {
// Exiting on a failed rebind must still flush crawl state.
stopCrawls()
if saveErr := state.S.Save(); saveErr != nil {
log.WithError(saveErr).Error("Failed to save state while shutting down.")
}
return err
}
}
}
// Shut down: stop the listener, stop the loops, then flush state.
log.Info("Shutting down.")
stopHTTP(httpServer)
stopCrawls()
return state.S.Save()
}