fix(server,app,config): address code review findings

- Fix FD leak in DupListener: close *os.File after net.FileListener
- Add cleanup of partially-duped listeners on DupListener failure
- Make reload timeout configurable via shutdown.reload_timeout
- Handle filepath.Abs errors in processIncludes instead of ignoring
- Use net.ParseIP in isAnyAddr for robust IPv6 support
This commit is contained in:
xfy 2026-06-03 13:16:05 +08:00
parent 9b8ce2a08a
commit 728a9f454b
5 changed files with 31 additions and 4 deletions

View File

@ -187,6 +187,9 @@ func (a *App) reloadConfig() {
for i, ln := range listeners { for i, ln := range listeners {
duped[i], err = server.DupListener(ln) duped[i], err = server.DupListener(ln)
if err != nil { if err != nil {
for j := 0; j < i; j++ {
_ = duped[j].Close()
}
a.logger.Error().Err(err).Msg("Failed to dup listener for reload") a.logger.Error().Err(err).Msg("Failed to dup listener for reload")
return return
} }
@ -205,6 +208,11 @@ func (a *App) reloadConfig() {
} }
}() }()
reloadTimeout := a.cfg.Shutdown.ReloadTimeout
if reloadTimeout <= 0 {
reloadTimeout = 5 * time.Second
}
select { select {
case err := <-startErr: case err := <-startErr:
a.logger.Error().Err(err).Msg("Failed to start new server with reloaded config") a.logger.Error().Err(err).Msg("Failed to start new server with reloaded config")
@ -212,7 +220,7 @@ func (a *App) reloadConfig() {
_ = ln.Close() _ = ln.Close()
} }
return return
case <-time.After(5 * time.Second): case <-time.After(reloadTimeout):
} }
oldSrv := a.srv oldSrv := a.srv

View File

@ -140,7 +140,10 @@ func Load(path string) (*Config, error) {
} }
if len(cfg.Include) > 0 { if len(cfg.Include) > 0 {
absPath, _ := filepath.Abs(path) absPath, err := filepath.Abs(path)
if err != nil {
return nil, fmt.Errorf("获取配置文件绝对路径失败: %w", err)
}
visited := map[string]bool{absPath: true} visited := map[string]bool{absPath: true}
if err := processIncludes(&cfg, filepath.Dir(path), 0, visited); err != nil { if err := processIncludes(&cfg, filepath.Dir(path), 0, visited); err != nil {
return nil, fmt.Errorf("处理配置引入失败: %w", err) return nil, fmt.Errorf("处理配置引入失败: %w", err)
@ -176,7 +179,10 @@ func processIncludes(cfg *Config, baseDir string, depth int, visited map[string]
} }
for _, match := range matches { for _, match := range matches {
absMatch, _ := filepath.Abs(match) absMatch, err := filepath.Abs(match)
if err != nil {
return fmt.Errorf("获取引入文件绝对路径失败 %q: %w", match, err)
}
if visited[absMatch] { if visited[absMatch] {
return fmt.Errorf("检测到循环引入: %s", absMatch) return fmt.Errorf("检测到循环引入: %s", absMatch)
} }

View File

@ -222,6 +222,7 @@ func DefaultConfig() *Config {
Shutdown: ShutdownConfig{ Shutdown: ShutdownConfig{
GracefulTimeout: 30 * time.Second, GracefulTimeout: 30 * time.Second,
FastTimeout: 5 * time.Second, FastTimeout: 5 * time.Second,
ReloadTimeout: 5 * time.Second,
}, },
} }
} }
@ -612,6 +613,7 @@ func GenerateConfigYAML(cfg *Config) ([]byte, error) {
buf.WriteString("shutdown:\n") buf.WriteString("shutdown:\n")
fmt.Fprintf(&buf, " graceful_timeout: %ds # 优雅停止超时SIGQUIT等待活跃请求完成0=使用默认30s\n", int(cfg.Shutdown.GracefulTimeout.Seconds())) fmt.Fprintf(&buf, " graceful_timeout: %ds # 优雅停止超时SIGQUIT等待活跃请求完成0=使用默认30s\n", int(cfg.Shutdown.GracefulTimeout.Seconds()))
fmt.Fprintf(&buf, " fast_timeout: %ds # 快速停止超时SIGINT/SIGTERM0=使用默认5s\n", int(cfg.Shutdown.FastTimeout.Seconds())) fmt.Fprintf(&buf, " fast_timeout: %ds # 快速停止超时SIGINT/SIGTERM0=使用默认5s\n", int(cfg.Shutdown.FastTimeout.Seconds()))
fmt.Fprintf(&buf, " reload_timeout: %ds # 热重载启动等待超时SIGHUP0=使用默认5s\n", int(cfg.Shutdown.ReloadTimeout.Seconds()))
buf.WriteString("\n") buf.WriteString("\n")
// stream 配置 // stream 配置

View File

@ -181,6 +181,11 @@ type ShutdownConfig struct {
// 接收到 SIGINT 或 SIGTERM 信号后,等待服务器关闭的最大时间 // 接收到 SIGINT 或 SIGTERM 信号后,等待服务器关闭的最大时间
// 默认: 5s当值为 0 时使用默认值) // 默认: 5s当值为 0 时使用默认值)
FastTimeout time.Duration `yaml:"fast_timeout"` FastTimeout time.Duration `yaml:"fast_timeout"`
// ReloadTimeout 热重载启动等待超时SIGHUP
// 等待新服务器启动完成的最大时间,超时后视为启动成功
// 默认: 5s当值为 0 时使用默认值)
ReloadTimeout time.Duration `yaml:"reload_timeout"`
} }
// ResolverConfig DNS 解析器配置。 // ResolverConfig DNS 解析器配置。

View File

@ -409,7 +409,11 @@ func (s *Server) tcpAddrMatch(inherited, target string) bool {
} }
func isAnyAddr(host string) bool { func isAnyAddr(host string) bool {
return host == "" || host == "0.0.0.0" || host == "::" || host == "[::]" if host == "" {
return true
}
ip := net.ParseIP(host)
return ip != nil && ip.IsUnspecified()
} }
// DupListener 复制 listener 的文件描述符,返回独立的 listener。 // DupListener 复制 listener 的文件描述符,返回独立的 listener。
@ -422,12 +426,14 @@ func DupListener(ln net.Listener) (net.Listener, error) {
if err != nil { if err != nil {
return nil, fmt.Errorf("dup tcp listener: %w", err) return nil, fmt.Errorf("dup tcp listener: %w", err)
} }
defer file.Close()
return net.FileListener(file) return net.FileListener(file)
case *net.UnixListener: case *net.UnixListener:
file, err := l.File() file, err := l.File()
if err != nil { if err != nil {
return nil, fmt.Errorf("dup unix listener: %w", err) return nil, fmt.Errorf("dup unix listener: %w", err)
} }
defer file.Close()
return net.FileListener(file) return net.FileListener(file)
default: default:
return nil, fmt.Errorf("unsupported listener type: %T", ln) return nil, fmt.Errorf("unsupported listener type: %T", ln)