首次提交:初始化项目代码
This commit is contained in:
@@ -0,0 +1,250 @@
|
||||
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")
|
||||
Reference in New Issue
Block a user