Files
cloudnest/internal/bootstrap/server.go
T

251 lines
7.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package bootstrap
import (
"context"
"errors"
"fmt"
"net/http"
"os"
"os/signal"
"sync"
"syscall"
"time"
"cloudnest/internal/config"
"cloudnest/internal/pkg/logger"
"github.com/gin-gonic/gin"
)
// Server encapsulates HTTP server lifecycle management with graceful start/restart/shutdown.
// Server 封装了 HTTP 服务器的生命周期管理,支持优雅启动/重启/关闭。
//
// Features:
// 1. Graceful startup with port auto-detection in dev mode
// 2. Graceful shutdown on SIGINT/SIGTERM signals
// 3. Hot reload on SIGHUP signal (Linux/Mac only) - re-reads config
// 4. Waits for in-flight requests to complete before shutdown
//
// 特性:
// 1. 开发模式下优雅启动,自动检测可用端口
// 2. 收到 SIGINT/SIGTERM 信号时优雅关闭
// 3. 收到 SIGHUP 信号时热重载(仅 Linux/Mac),重新读取配置
// 4. 关闭前等待正在进行的请求完成
type Server struct {
cfg *config.Config
engine *gin.Engine
srv *http.Server
mu sync.RWMutex
reloadChan chan os.Signal
shutdownOnce sync.Once
}
// NewServer creates a new Server instance with the given config and engine.
// NewServer 使用给定的配置和引擎创建新的 Server 实例。
//
// Parameters:
// - cfg: The application configuration.
// - engine: The Gin engine to serve.
//
// Returns:
// - *Server: A new Server instance.
//
// 参数:
// - cfg: 应用程序配置。
// - engine: 要服务的 Gin 引擎。
//
// 返回值:
// - *Server: 新的 Server 实例。
func NewServer(cfg *config.Config, engine *gin.Engine) *Server {
return &Server{
cfg: cfg,
engine: engine,
reloadChan: make(chan os.Signal, 1),
}
}
// Run starts the server and blocks until a shutdown signal is received.
// Run 启动服务器并阻塞,直到收到关闭信号。
//
// Lifecycle:
// 1. Auto-detect available port (dev mode only)
// 2. Start listening in a goroutine
// 3. Register signal handlers (SIGINT, SIGTERM for shutdown; SIGHUP for reload on Unix)
// 4. Block until shutdown signal
// 5. Gracefully shut down with timeout
//
// 生命周期:
// 1. 自动检测可用端口(仅开发模式)
// 2. 在 goroutine 中开始监听
// 3. 注册信号处理器(SIGINT、SIGTERM 用于关闭;SIGHUP 用于 Unix 上的重载)
// 4. 阻塞直到收到关闭信号
// 5. 使用超时优雅关闭
//
// Returns:
// - error: An error if startup or shutdown fails.
//
// 返回值:
// - error: 如果启动或关闭失败则返回错误。
func (s *Server) Run() error {
port := s.resolvePort()
addr := fmt.Sprintf("%s:%d", s.cfg.Server.Host, port)
s.mu.Lock()
s.srv = &http.Server{
Addr: addr,
Handler: s.engine,
}
s.mu.Unlock()
// Register signal handlers / 注册信号处理器
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
// Register SIGHUP for hot reload (Unix only)
// 注册 SIGHUP 用于热重载(仅 Unix
if isUnix() {
signal.Notify(s.reloadChan, syscall.SIGHUP)
}
// Start server in a goroutine / 在 goroutine 中启动服务器
serverErr := make(chan error, 1)
go func() {
logger.Info("server listening", "address", addr, "env", s.cfg.App.Env)
if err := s.srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
serverErr <- err
}
close(serverErr)
}()
// Wait for signal / 等待信号
select {
case sig := <-sigChan:
logger.Info("shutdown signal received", "signal", sig.String())
return s.Shutdown()
case sig := <-s.reloadChan:
logger.Info("reload signal received, restarting", "signal", sig.String())
if err := s.Shutdown(); err != nil {
return err
}
// After shutdown, the caller should restart the process
// 关闭后,调用方应重启进程
return ErrReloadRequested
case err := <-serverErr:
if err != nil {
logger.Error("server failed", "error", err)
return err
}
return nil
}
}
// Shutdown gracefully shuts down the server with a timeout.
// Shutdown 使用超时优雅关闭服务器。
//
// Parameters:
// - timeout: The maximum time to wait for in-flight requests to complete.
//
// Returns:
// - error: An error if shutdown fails.
//
// Note:
// - Safe to call multiple times (subsequent calls are no-ops).
//
// 参数:
// - timeout: 等待正在进行的请求完成的最长时间。
//
// 返回值:
// - error: 如果关闭失败则返回错误。
//
// 注意:
// - 可以安全地多次调用(后续调用是空操作)。
func (s *Server) Shutdown() error {
var shutdownErr error
s.shutdownOnce.Do(func() {
s.mu.RLock()
srv := s.srv
s.mu.RUnlock()
if srv == nil {
return
}
timeout := time.Duration(s.cfg.Server.GracefulShutdownTimeout) * time.Second
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
logger.Info("graceful shutdown started", "timeout_seconds", timeout)
if err := srv.Shutdown(ctx); err != nil {
logger.Error("graceful shutdown failed, forcing close", "error", err)
if closeErr := srv.Close(); closeErr != nil {
shutdownErr = fmt.Errorf("forced close failed: %w (original: %v)", closeErr, err)
return
}
shutdownErr = err
}
logger.Info("server exited gracefully")
})
return shutdownErr
}
// resolvePort finds an available port starting from the configured port.
// resolvePort 从配置的端口开始查找可用端口。
//
// In dev mode: tries configured port, then port+1, port+2, ..., up to +9.
// In prod mode: uses the configured port directly (no auto-switch).
//
// 开发模式:尝试配置端口,然后 port+1, port+2, ..., 最多 +9。
// 生产模式:直接使用配置的端口(不自动切换)。
//
// Returns:
// - int: The actual port to use.
//
// 返回值:
// - int: 实际使用的端口。
func (s *Server) resolvePort() int {
port := s.cfg.Server.Port
if !s.cfg.IsDev() {
return port
}
availablePort, err := config.FindAvailablePort(port, 10)
if err != nil {
logger.Fatal("no available port found", "preferred", port, "error", err)
}
if availablePort != port {
logger.Info("port auto-switched", "from", port, "to", availablePort)
}
return availablePort
}
// Addr returns the actual server address (host:port) after resolvePort has been called.
// Addr 返回调用 resolvePort 后的实际服务器地址(host:port)。
//
// Returns:
// - string: The server address, or empty string if server hasn't started.
//
// 返回值:
// - string: 服务器地址,如果服务器尚未启动则返回空字符串。
func (s *Server) Addr() string {
s.mu.RLock()
defer s.mu.RUnlock()
if s.srv == nil {
return ""
}
return s.srv.Addr
}
// isUnix returns true if the current OS is Unix-like (Linux, macOS, BSD, etc.).
// isUnix 如果当前操作系统是类 Unix 系统(Linux、macOS、BSD 等)则返回 true。
func isUnix() bool {
return os.PathSeparator == '/'
}
// ErrReloadRequested is returned by Run() when a SIGHUP signal requests a reload.
// ErrReloadRequested 当 SIGHUP 信号请求重载时由 Run() 返回。
//
// The caller should typically exit with a non-zero code so the process supervisor
// (systemd, Docker, k8s) can restart the application with the new config.
//
// 调用方通常应以非零退出码退出,以便进程管理器(systemd、Docker、k8s)可以使用新配置重启应用程序。
var ErrReloadRequested = errors.New("reload requested")