feat(server,app): implement proper config hot reload via SIGHUP
createListener now checks pre-set s.listeners (Path 2) for hot reload, not just upgradeManager.IsChild() (Path 1). Add DupListener to dup FDs so old/new servers own independent listeners. Reload rebuilds HTTP/2 and HTTP/3. Add matchInheritedListener with TCP any-addr matching. Add requiresFullRestart with VHost server count detection.
This commit is contained in:
parent
2e9ddc7400
commit
f3f78b24a8
@ -4,6 +4,7 @@ package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
@ -164,11 +165,121 @@ func (a *App) reloadConfig() {
|
||||
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 {
|
||||
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
|
||||
}
|
||||
}()
|
||||
|
||||
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(5 * time.Second):
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (a *App) gracefulUpgrade() {
|
||||
execPath, err := os.Executable()
|
||||
if err != nil {
|
||||
|
||||
@ -461,7 +461,6 @@ func TestHandleSignal_SIGINT(t *testing.T) {
|
||||
|
||||
// TestHandleSignal_SIGHUP 测试 SIGHUP 信号处理(重载配置)
|
||||
func TestHandleSignal_SIGHUP(t *testing.T) {
|
||||
// 创建临时配置文件
|
||||
tmpDir := t.TempDir()
|
||||
cfgPath := filepath.Join(tmpDir, "config.yaml")
|
||||
cfgContent := `
|
||||
@ -1448,14 +1447,12 @@ logging:
|
||||
}
|
||||
app.logger = setupTestLogger()
|
||||
|
||||
// 发送 SIGHUP 信号
|
||||
result := app.handleSignal(syscall.SIGHUP)
|
||||
|
||||
if result != true {
|
||||
t.Error("Expected handleSignal(SIGHUP) to return true")
|
||||
}
|
||||
|
||||
// 验证配置已更新
|
||||
if app.cfg.Servers[0].Listen != ":7070" {
|
||||
t.Errorf("Expected listen ':7070', got '%s'", app.cfg.Servers[0].Listen)
|
||||
}
|
||||
|
||||
@ -318,32 +318,31 @@ func (s *Server) Start() error {
|
||||
func (s *Server) createListener(cfg *config.ServerConfig) (net.Listener, error) {
|
||||
listenAddr := cfg.Listen
|
||||
|
||||
if s.upgradeManager != nil && s.upgradeManager.IsChild() {
|
||||
inherited, _ := s.upgradeManager.GetInheritedListeners()
|
||||
if ln := s.matchInheritedListener(inherited, listenAddr); ln != nil {
|
||||
return ln, nil
|
||||
}
|
||||
}
|
||||
|
||||
if len(s.listeners) > 0 {
|
||||
if ln := s.matchInheritedListener(s.listeners, listenAddr); ln != nil {
|
||||
return ln, nil
|
||||
}
|
||||
}
|
||||
|
||||
if strings.HasPrefix(listenAddr, "unix:") {
|
||||
// Unix Socket 模式
|
||||
socketPath := listenAddr[5:]
|
||||
|
||||
// 1. 检查继承的监听器(热升级场景)
|
||||
if s.upgradeManager != nil && s.upgradeManager.IsChild() {
|
||||
inherited, _ := s.upgradeManager.GetInheritedListeners()
|
||||
for _, ln := range inherited {
|
||||
if ln.Addr().Network() == "unix" && ln.Addr().String() == socketPath {
|
||||
return ln, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 清理旧 socket 文件
|
||||
if _, err := os.Stat(socketPath); err == nil {
|
||||
_ = os.Remove(socketPath)
|
||||
}
|
||||
|
||||
// 3. 创建 Unix socket listener
|
||||
listener, err := net.Listen("unix", socketPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create unix socket failed: %w", err)
|
||||
}
|
||||
|
||||
// 4. 设置 socket 文件权限
|
||||
mode := 0o666
|
||||
if cfg.UnixSocket.Mode > 0 {
|
||||
mode = cfg.UnixSocket.Mode
|
||||
@ -352,19 +351,89 @@ func (s *Server) createListener(cfg *config.ServerConfig) (net.Listener, error)
|
||||
logging.Warn().Err(err).Msg("Failed to set socket file permissions")
|
||||
}
|
||||
|
||||
// 5. 设置文件所有权(需要 root 权限)
|
||||
if cfg.UnixSocket.User != "" || cfg.UnixSocket.Group != "" {
|
||||
// 简化处理:仅记录警告,实际实现需要 syscall.Chown
|
||||
logging.Warn().Msg("Unix socket user/group config requires root privileges, skipped")
|
||||
}
|
||||
|
||||
return listener, nil
|
||||
}
|
||||
|
||||
// TCP 模式
|
||||
return net.Listen("tcp", listenAddr)
|
||||
}
|
||||
|
||||
func (s *Server) matchInheritedListener(inherited []net.Listener, listenAddr string) net.Listener {
|
||||
if len(inherited) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
if strings.HasPrefix(listenAddr, "unix:") {
|
||||
socketPath := listenAddr[5:]
|
||||
for _, ln := range inherited {
|
||||
if ln == nil {
|
||||
continue
|
||||
}
|
||||
if ln.Addr().Network() == "unix" && ln.Addr().String() == socketPath {
|
||||
return ln
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, ln := range inherited {
|
||||
if ln == nil {
|
||||
continue
|
||||
}
|
||||
if ln.Addr().Network() != "tcp" {
|
||||
continue
|
||||
}
|
||||
if s.tcpAddrMatch(ln.Addr().String(), listenAddr) {
|
||||
return ln
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) tcpAddrMatch(inherited, target string) bool {
|
||||
if inherited == target {
|
||||
return true
|
||||
}
|
||||
host1, port1, err1 := net.SplitHostPort(inherited)
|
||||
host2, port2, err2 := net.SplitHostPort(target)
|
||||
if err1 != nil || err2 != nil {
|
||||
return false
|
||||
}
|
||||
if port1 != port2 {
|
||||
return false
|
||||
}
|
||||
return host1 == host2 || isAnyAddr(host1) || isAnyAddr(host2)
|
||||
}
|
||||
|
||||
func isAnyAddr(host string) bool {
|
||||
return host == "" || host == "0.0.0.0" || host == "::" || host == "[::]"
|
||||
}
|
||||
|
||||
// DupListener 复制 listener 的文件描述符,返回独立的 listener。
|
||||
//
|
||||
// 用于热重载场景:新旧 server 各自持有独立 FD,互不影响关闭操作。
|
||||
func DupListener(ln net.Listener) (net.Listener, error) {
|
||||
switch l := ln.(type) {
|
||||
case *net.TCPListener:
|
||||
file, err := l.File()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("dup tcp listener: %w", err)
|
||||
}
|
||||
return net.FileListener(file)
|
||||
case *net.UnixListener:
|
||||
file, err := l.File()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("dup unix listener: %w", err)
|
||||
}
|
||||
return net.FileListener(file)
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported listener type: %T", ln)
|
||||
}
|
||||
}
|
||||
|
||||
// startSingleMode 单服务器模式启动。
|
||||
//
|
||||
// 在单服务器模式下,创建单一路由器,注册代理路由和静态文件服务,
|
||||
|
||||
@ -989,6 +989,136 @@ func TestCreateListener_UnixSocketCleanup(t *testing.T) {
|
||||
defer ln.Close()
|
||||
}
|
||||
|
||||
func TestDupListener_TCP(t *testing.T) {
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer ln.Close()
|
||||
|
||||
duped, err := DupListener(ln)
|
||||
if err != nil {
|
||||
t.Fatalf("DupListener() error: %v", err)
|
||||
}
|
||||
defer duped.Close()
|
||||
|
||||
if duped.Addr().Network() != "tcp" {
|
||||
t.Errorf("expected tcp, got %s", duped.Addr().Network())
|
||||
}
|
||||
if duped.Addr().String() != ln.Addr().String() {
|
||||
t.Errorf("expected same address %s, got %s", ln.Addr().String(), duped.Addr().String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestDupListener_Unix(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
socketPath := dir + "/dup.sock"
|
||||
ln, err := net.Listen("unix", socketPath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer ln.Close()
|
||||
|
||||
duped, err := DupListener(ln)
|
||||
if err != nil {
|
||||
t.Fatalf("DupListener() error: %v", err)
|
||||
}
|
||||
defer duped.Close()
|
||||
}
|
||||
|
||||
func TestDupListener_Unsupported(t *testing.T) {
|
||||
_, err := DupListener(struct{ net.Listener }{})
|
||||
if err == nil {
|
||||
t.Error("expected error for unsupported type")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTcpAddrMatch(t *testing.T) {
|
||||
s := &Server{}
|
||||
|
||||
tests := []struct {
|
||||
inherited string
|
||||
target string
|
||||
want bool
|
||||
}{
|
||||
{"127.0.0.1:8080", "127.0.0.1:8080", true},
|
||||
{"0.0.0.0:8080", ":8080", true},
|
||||
{"[::]:8080", ":8080", true},
|
||||
{"0.0.0.0:8080", "0.0.0.0:8080", true},
|
||||
{"0.0.0.0:8080", "127.0.0.1:8080", true},
|
||||
{"127.0.0.1:8080", "0.0.0.0:8080", true},
|
||||
{"127.0.0.1:8080", ":9090", false},
|
||||
{"127.0.0.1:8080", "192.168.1.1:8080", false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
got := s.tcpAddrMatch(tt.inherited, tt.target)
|
||||
if got != tt.want {
|
||||
t.Errorf("tcpAddrMatch(%q, %q) = %v, want %v", tt.inherited, tt.target, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMatchInheritedListener_TCP(t *testing.T) {
|
||||
s := &Server{}
|
||||
|
||||
ln1, _ := net.Listen("tcp", "127.0.0.1:0")
|
||||
defer ln1.Close()
|
||||
|
||||
ln2, _ := net.Listen("tcp", "127.0.0.1:0")
|
||||
defer ln2.Close()
|
||||
|
||||
inherited := []net.Listener{ln1, ln2}
|
||||
|
||||
result := s.matchInheritedListener(inherited, "0.0.0.0:99999")
|
||||
if result != nil {
|
||||
t.Error("expected nil for non-matching address")
|
||||
}
|
||||
|
||||
addr1 := ln1.Addr().String()
|
||||
result = s.matchInheritedListener(inherited, addr1)
|
||||
if result != ln1 {
|
||||
t.Errorf("expected ln1 for address %s", addr1)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMatchInheritedListener_Empty(t *testing.T) {
|
||||
s := &Server{}
|
||||
result := s.matchInheritedListener(nil, ":8080")
|
||||
if result != nil {
|
||||
t.Error("expected nil for empty inherited list")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMatchInheritedListener_PresetListeners(t *testing.T) {
|
||||
cfg := &config.Config{
|
||||
Servers: []config.ServerConfig{{Listen: "127.0.0.1:0"}},
|
||||
}
|
||||
s := New(cfg)
|
||||
|
||||
ln, err := s.createListener(&cfg.Servers[0])
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer ln.Close()
|
||||
|
||||
s.SetListeners([]net.Listener{ln})
|
||||
|
||||
addr := ln.Addr().String()
|
||||
cfg.Servers[0].Listen = addr
|
||||
|
||||
matched, err := s.createListener(&cfg.Servers[0])
|
||||
if err != nil {
|
||||
t.Fatalf("createListener with preset should reuse: %v", err)
|
||||
}
|
||||
if matched == nil {
|
||||
t.Fatal("expected non-nil listener from preset match")
|
||||
}
|
||||
if matched.Addr().String() != addr {
|
||||
t.Errorf("expected same address %s, got %s", addr, matched.Addr().String())
|
||||
}
|
||||
}
|
||||
|
||||
// TestServer_StatsMethods 测试服务器统计方法。
|
||||
func TestServer_StatsMethods(t *testing.T) {
|
||||
cfg := &config.Config{
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user