- Fix handleConnection to use addr parameter for direct upstream map lookup instead of always selecting the first upstream - Add Server.Stop() for graceful shutdown with listener closing, UDP server cleanup, health checker termination, and goroutine joining - Add shutdownStream() to App and call it in SIGTERM/SIGQUIT/SIGUSR2 signal handlers to prevent goroutine and port leaks on shutdown
359 lines
7.9 KiB
Go
359 lines
7.9 KiB
Go
//go:build !windows
|
||
|
||
// Package app 提供 Lolly 应用程序的生命周期管理和命令行入口。
|
||
//
|
||
// 包含应用生命周期管理和命令行入口相关的逻辑。
|
||
//
|
||
// 作者:xfy
|
||
package app
|
||
|
||
import (
|
||
"fmt"
|
||
"net"
|
||
"os"
|
||
"os/signal"
|
||
"syscall"
|
||
"time"
|
||
|
||
"rua.plus/lolly/internal/config"
|
||
"rua.plus/lolly/internal/logging"
|
||
"rua.plus/lolly/internal/server"
|
||
)
|
||
|
||
// Run starts the application: loads config, creates servers, and handles signals.
|
||
func (a *App) Run() int {
|
||
if err := a.loadAndValidateConfig(); err != nil {
|
||
return 1
|
||
}
|
||
|
||
a.initVariables()
|
||
|
||
// Inherit parent listeners when running as a graceful upgrade child.
|
||
a.inheritListeners()
|
||
|
||
a.logServerAddresses()
|
||
a.initResolver()
|
||
a.initServer()
|
||
a.initStreamServers()
|
||
a.initHTTP3()
|
||
a.initHTTP2()
|
||
|
||
a.upgradeMgr = server.NewUpgradeManager(a.srv)
|
||
a.srv.SetUpgradeManager(a.upgradeMgr)
|
||
if a.pidFile != "" {
|
||
a.upgradeMgr.SetPidFile(a.pidFile)
|
||
_ = a.upgradeMgr.WritePid()
|
||
}
|
||
|
||
a.signalReady()
|
||
|
||
sigChan := make(chan os.Signal, 1)
|
||
a.setupSignalHandlers(sigChan)
|
||
|
||
errChan := make(chan error, 1)
|
||
go func() {
|
||
a.logger.LogStartup("Starting HTTP server", nil)
|
||
if err := a.srv.Start(); err != nil {
|
||
errChan <- err
|
||
}
|
||
}()
|
||
|
||
sigintCount := 0
|
||
|
||
for {
|
||
select {
|
||
case err := <-errChan:
|
||
a.logger.Error().Err(err).Msg("Server failed to start")
|
||
return 1
|
||
case sig := <-sigChan:
|
||
if sig == syscall.SIGINT {
|
||
sigintCount++
|
||
if sigintCount >= 3 {
|
||
a.logger.LogShutdown("Received 3 SIGINT, forcing exit")
|
||
return 1
|
||
}
|
||
}
|
||
if !a.handleSignal(sig) {
|
||
a.logger.LogShutdown("Server stopped")
|
||
return 0
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// inheritListeners inherits parent listeners during graceful upgrade.
|
||
func (a *App) inheritListeners() {
|
||
if os.Getenv("GRACEFUL_UPGRADE") == "1" {
|
||
a.logger.LogStartup("Graceful upgrade mode detected, inheriting parent listeners", nil)
|
||
a.upgradeMgr = server.NewUpgradeManager(nil)
|
||
listeners, err := a.upgradeMgr.GetInheritedListeners()
|
||
if err == nil && len(listeners) > 0 {
|
||
a.listeners = listeners
|
||
}
|
||
}
|
||
}
|
||
|
||
func (a *App) setupSignalHandlers(sigChan chan<- os.Signal) {
|
||
signal.Notify(sigChan,
|
||
syscall.SIGTERM,
|
||
syscall.SIGINT,
|
||
syscall.SIGQUIT,
|
||
syscall.SIGHUP,
|
||
syscall.SIGUSR1,
|
||
syscall.SIGUSR2,
|
||
)
|
||
}
|
||
|
||
// handleSignal returns false to indicate the app should exit.
|
||
func (a *App) handleSignal(sig os.Signal) bool {
|
||
if a.cfg == nil {
|
||
a.logger.Error().Msg("Signal handling failed: config is nil, using default timeout")
|
||
a.cfg = &config.Config{
|
||
Shutdown: config.ShutdownConfig{
|
||
GracefulTimeout: 30 * time.Second,
|
||
FastTimeout: 5 * time.Second,
|
||
},
|
||
}
|
||
}
|
||
|
||
switch sig {
|
||
case syscall.SIGQUIT:
|
||
timeout := a.cfg.Shutdown.GracefulTimeout
|
||
if timeout <= 0 {
|
||
timeout = 30 * time.Second
|
||
}
|
||
a.logger.LogSignal("SIGQUIT", fmt.Sprintf("Graceful stop (waiting %v)", timeout))
|
||
a.shutdownStream()
|
||
a.shutdownHTTP2()
|
||
a.shutdownHTTP3()
|
||
_ = a.srv.GracefulStop(timeout)
|
||
return false
|
||
|
||
case syscall.SIGTERM, syscall.SIGINT:
|
||
timeout := a.cfg.Shutdown.FastTimeout
|
||
if timeout <= 0 {
|
||
timeout = 5 * time.Second
|
||
}
|
||
sigTyped, ok := sig.(syscall.Signal)
|
||
if !ok {
|
||
a.logger.LogSignal("unknown", "Stopping server")
|
||
} else {
|
||
a.logger.LogSignal(sigName(sigTyped), "Stopping server")
|
||
}
|
||
a.shutdownStream()
|
||
a.shutdownHTTP2()
|
||
a.shutdownHTTP3()
|
||
_ = a.srv.StopWithTimeout(timeout)
|
||
return false
|
||
|
||
case syscall.SIGHUP:
|
||
a.logger.LogSignal("SIGHUP", "Reloading config")
|
||
a.reloadConfig()
|
||
return true
|
||
|
||
case syscall.SIGUSR1:
|
||
a.logger.LogSignal("SIGUSR1", "Reopening logs")
|
||
a.reopenLogs()
|
||
return true
|
||
|
||
case syscall.SIGUSR2:
|
||
a.logger.LogSignal("SIGUSR2", "Performing graceful upgrade")
|
||
a.gracefulUpgrade()
|
||
return true
|
||
|
||
default:
|
||
a.logger.Info().Str("signal", sig.String()).Msg("Received unknown signal")
|
||
return true
|
||
}
|
||
}
|
||
|
||
func (a *App) reloadConfig() {
|
||
newCfg, err := config.Load(a.cfgPath)
|
||
if err != nil {
|
||
a.logger.Error().Err(err).Msg("Failed to reload config")
|
||
return
|
||
}
|
||
|
||
if a.srv == nil {
|
||
a.cfg = newCfg
|
||
a.logger = logging.NewAppLogger(&newCfg.Logging)
|
||
a.logger.LogStartup("Config reloaded (no running server)", nil)
|
||
return
|
||
}
|
||
|
||
if a.requiresFullRestart(newCfg) {
|
||
logging.Warn().Msg("Config requires full restart (listen address or mode changed). Use SIGUSR2 for graceful upgrade.")
|
||
return
|
||
}
|
||
|
||
listeners := a.srv.GetListeners()
|
||
if len(listeners) == 0 {
|
||
a.logger.Error().Msg("Cannot reload: server has no saved listeners")
|
||
return
|
||
}
|
||
|
||
duped := make([]net.Listener, len(listeners))
|
||
for i, ln := range listeners {
|
||
duped[i], err = server.DupListener(ln)
|
||
if err != nil {
|
||
for j := range i {
|
||
_ = duped[j].Close()
|
||
}
|
||
a.logger.Error().Err(err).Msg("Failed to dup listener for reload")
|
||
return
|
||
}
|
||
}
|
||
|
||
newSrv := server.New(newCfg)
|
||
if a.resv != nil {
|
||
newSrv.SetResolver(a.resv)
|
||
}
|
||
newSrv.SetListeners(duped)
|
||
|
||
startErr := make(chan error, 1)
|
||
go func() {
|
||
if err := newSrv.Start(); err != nil {
|
||
startErr <- err
|
||
}
|
||
}()
|
||
|
||
reloadTimeout := a.cfg.Shutdown.ReloadTimeout
|
||
if reloadTimeout <= 0 {
|
||
reloadTimeout = 5 * time.Second
|
||
}
|
||
|
||
select {
|
||
case err := <-startErr:
|
||
a.logger.Error().Err(err).Msg("Failed to start new server with reloaded config")
|
||
for _, ln := range duped {
|
||
_ = ln.Close()
|
||
}
|
||
return
|
||
case <-time.After(reloadTimeout):
|
||
}
|
||
|
||
oldSrv := a.srv
|
||
oldHTTP2 := a.http2Srv
|
||
oldHTTP3 := a.http3Srv
|
||
|
||
a.srv = newSrv
|
||
a.cfg = newCfg
|
||
a.logger = logging.NewAppLogger(&newCfg.Logging)
|
||
a.http2Srv = nil
|
||
a.http3Srv = nil
|
||
|
||
a.initVariables()
|
||
a.initHTTP2()
|
||
a.initHTTP3()
|
||
|
||
if a.upgradeMgr != nil {
|
||
a.upgradeMgr.SetListeners(newSrv.GetListeners())
|
||
}
|
||
|
||
go func() {
|
||
if oldHTTP2 != nil {
|
||
_ = oldHTTP2.Stop()
|
||
}
|
||
if oldHTTP3 != nil {
|
||
_ = oldHTTP3.Stop()
|
||
}
|
||
_ = oldSrv.GracefulStop(30 * time.Second)
|
||
}()
|
||
|
||
a.logger.LogStartup("Config reloaded successfully", nil)
|
||
}
|
||
|
||
func (a *App) requiresFullRestart(newCfg *config.Config) bool {
|
||
if a.cfg.GetMode() != newCfg.GetMode() {
|
||
return true
|
||
}
|
||
oldMode := a.cfg.GetMode()
|
||
switch oldMode {
|
||
case config.ServerModeSingle:
|
||
if len(a.cfg.Servers) > 0 && len(newCfg.Servers) > 0 {
|
||
if a.cfg.Servers[0].Listen != newCfg.Servers[0].Listen {
|
||
return true
|
||
}
|
||
}
|
||
case config.ServerModeVHost:
|
||
if len(a.cfg.Servers) != len(newCfg.Servers) {
|
||
return true
|
||
}
|
||
if len(a.cfg.Servers) > 0 && len(newCfg.Servers) > 0 {
|
||
if a.cfg.Servers[0].Listen != newCfg.Servers[0].Listen {
|
||
return true
|
||
}
|
||
}
|
||
case config.ServerModeMultiServer:
|
||
if len(a.cfg.Servers) != len(newCfg.Servers) {
|
||
return true
|
||
}
|
||
for i := range a.cfg.Servers {
|
||
if a.cfg.Servers[i].Listen != newCfg.Servers[i].Listen {
|
||
return true
|
||
}
|
||
}
|
||
case config.ServerModeAuto:
|
||
return true
|
||
}
|
||
return false
|
||
}
|
||
|
||
func (a *App) gracefulUpgrade() {
|
||
execPath, err := os.Executable()
|
||
if err != nil {
|
||
a.logger.Error().Err(err).Msg("Failed to get executable path")
|
||
return
|
||
}
|
||
|
||
if a.srv == nil {
|
||
a.logger.Error().Msg("Graceful upgrade failed: server instance is nil")
|
||
return
|
||
}
|
||
|
||
listeners := a.srv.GetListeners()
|
||
if len(listeners) == 0 {
|
||
a.logger.Error().Msg("Graceful upgrade failed: server has no saved listeners (graceful upgrade not fully implemented)")
|
||
a.logger.Info().Msg("Hint: graceful upgrade requires the server to use manual listener management mode")
|
||
return
|
||
}
|
||
|
||
a.upgradeMgr.SetListeners(listeners)
|
||
|
||
if err := a.upgradeMgr.GracefulUpgrade(execPath); err != nil {
|
||
a.logger.Error().Err(err).Msg("Graceful upgrade failed")
|
||
return
|
||
}
|
||
|
||
a.logger.LogStartup("Graceful upgrade started, new process is taking over", nil)
|
||
|
||
timeout := a.cfg.Shutdown.GracefulTimeout
|
||
if timeout <= 0 {
|
||
timeout = 30 * time.Second
|
||
}
|
||
a.shutdownStream()
|
||
a.shutdownHTTP2()
|
||
a.shutdownHTTP3()
|
||
_ = a.srv.GracefulStop(timeout)
|
||
}
|
||
|
||
func sigName(sig syscall.Signal) string {
|
||
//nolint:exhaustive // Only handling app-relevant signals.
|
||
switch sig {
|
||
case syscall.SIGTERM:
|
||
return "SIGTERM"
|
||
case syscall.SIGINT:
|
||
return "SIGINT"
|
||
case syscall.SIGQUIT:
|
||
return "SIGQUIT"
|
||
case syscall.SIGHUP:
|
||
return "SIGHUP"
|
||
case syscall.SIGUSR1:
|
||
return "SIGUSR1"
|
||
case syscall.SIGUSR2:
|
||
return "SIGUSR2"
|
||
default:
|
||
return fmt.Sprintf("Signal(%d)", sig)
|
||
}
|
||
}
|