首次提交:初始化项目代码

This commit is contained in:
sunct
2026-07-31 15:24:14 +08:00
commit 8d3c97bd01
73 changed files with 11720 additions and 0 deletions
+250
View File
@@ -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")