refactor: use nginx -s reload instead of systemd

- doc: add comment
This commit is contained in:
2026-08-17 10:10:14 +08:00
parent 79f18fcdea
commit f536987a7e
62 changed files with 2216 additions and 598 deletions
+44 -29
View File
@@ -10,55 +10,55 @@ import (
"strings"
)
// Snapshot is the complete host Nginx configuration before or after a switch.
// Snapshot 切换前后完整宿主 Nginx 配置的快照,用于恢复与补偿。
type Snapshot struct {
Content []byte
// Content 配置文件的完整字节内容。
Content []byte
// ActivePort 内容中处于活动状态的后端端口。
ActivePort int
}
// Controller validates, atomically replaces, and reloads the current host Nginx configuration.
// Controller 负责校验、原子替换并重载当前宿主 Nginx 配置。
type Controller struct {
configPath string
nginxExecutable string
systemctlPath string
nginxServiceName string
runner commandRunner
// configPath 宿主 Nginx 配置文件的绝对路径。
configPath string
// nginxExecutable 用于配置校验的 Nginx 可执行文件绝对路径。
nginxExecutable string
// runner 执行外部命令的抽象,便于测试时替换。
runner commandRunner
}
// NewController requires every external identifier to be supplied explicitly.
func NewController(configPath string, nginxExecutable string, systemctlPath string, nginxServiceName string) (*Controller, error) {
return newController(configPath, nginxExecutable, systemctlPath, nginxServiceName, execRunner{})
// NewController 要求显式提供配置文件和 Nginx 可执行文件路径。
func NewController(configPath string, nginxExecutable string) (*Controller, error) {
return newController(configPath, nginxExecutable, execRunner{})
}
func newController(configPath string, nginxExecutable string, systemctlPath string, nginxServiceName string, runner commandRunner) (*Controller, error) {
// newController 校验各参数后创建 Controller。configPath、nginxExecutable、systemctlPath 必须是
// 干净绝对路径,nginxServiceName 必须非空且无首尾空白,runner 必须非空。
func newController(configPath string, nginxExecutable string, runner commandRunner) (*Controller, error) {
for _, entry := range []struct {
name string
value string
}{
{"host Nginx configuration", configPath},
{"Nginx executable", nginxExecutable},
{"systemctl executable", systemctlPath},
} {
if !filepath.IsAbs(entry.value) || filepath.Clean(entry.value) != entry.value {
return nil, fmt.Errorf("%s must be a clean absolute path", entry.name)
}
}
if nginxServiceName == "" || strings.TrimSpace(nginxServiceName) != nginxServiceName {
return nil, errors.New("exact Nginx systemd service name is required")
}
if runner == nil {
return nil, errors.New("host Nginx command runner is required")
}
return &Controller{
configPath: configPath,
nginxExecutable: nginxExecutable,
systemctlPath: systemctlPath,
nginxServiceName: nginxServiceName,
runner: runner,
configPath: configPath,
nginxExecutable: nginxExecutable,
runner: runner,
}, nil
}
// Read returns the complete current configuration and its active backend port.
// Read 返回当前完整配置内容及其活动后端端口。它要求配置文件是直接存在的普通文件
// (非符号链接),读取后调用 ActiveBackendPort 解析活动端口。
func (c *Controller) Read() (Snapshot, error) {
info, err := os.Lstat(c.configPath)
if err != nil {
@@ -78,8 +78,9 @@ func (c *Controller) Read() (Snapshot, error) {
return Snapshot{Content: content, ActivePort: port}, nil
}
// Switch renders activePort from the current configuration, then validates and reloads Nginx.
// It returns the durable pre-switch snapshot required for compensation.
// Switch 基于当前配置渲染出 activePort 为活动的配置,校验后原子替换并重载 Nginx
// 若当前活动端口已等于 activePort 则直接返回当前快照,不做任何改动。
// 返回的 Snapshot 是切换前的完整快照,供调用方在后续失败时补偿。
func (c *Controller) Switch(ctx context.Context, activePort int) (Snapshot, error) {
previous, err := c.Read()
if err != nil {
@@ -98,7 +99,8 @@ func (c *Controller) Switch(ctx context.Context, activePort int) (Snapshot, erro
return previous, nil
}
// Restore atomically restores a previously persisted complete configuration and reloads Nginx.
// Restore 原子恢复一份先前持久化的完整配置并重载 Nginx。它校验快照非空、
// 内容中的活动端口与快照元数据一致,且仅在与当前内容不同时才执行替换。
func (c *Controller) Restore(ctx context.Context, snapshot Snapshot) error {
if len(snapshot.Content) == 0 {
return errors.New("host Nginx restore snapshot is empty")
@@ -120,7 +122,8 @@ func (c *Controller) Restore(ctx context.Context, snapshot Snapshot) error {
return c.replaceValidateReload(ctx, snapshot.Content, current.Content)
}
// Apply installs one previously persisted complete snapshot and reloads Nginx.
// Apply 安装一份先前持久化的完整配置快照并重载 Nginx。它校验快照非空、
// 内容中的活动端口与快照元数据一致后直接替换当前配置。
func (c *Controller) Apply(ctx context.Context, snapshot Snapshot) error {
if len(snapshot.Content) == 0 {
return errors.New("host Nginx apply snapshot is empty")
@@ -139,6 +142,8 @@ func (c *Controller) Apply(ctx context.Context, snapshot Snapshot) error {
return c.replaceValidateReload(ctx, snapshot.Content, current.Content)
}
// replaceValidateReload 先原子写入 desired,再用 nginx -t 校验,最后通过 nginx -s reload
// 平滑重载 Nginx。校验或重载失败时都会回滚到 rollback 内容并把补偿错误合并返回。
func (c *Controller) replaceValidateReload(ctx context.Context, desired []byte, rollback []byte) error {
if err := c.atomicWrite(desired); err != nil {
return err
@@ -149,27 +154,31 @@ func (c *Controller) replaceValidateReload(ctx context.Context, desired []byte,
c.restoreAfterFailure(ctx, rollback),
)
}
if err := c.runner.Run(ctx, c.systemctlPath, "reload", "--", c.nginxServiceName); err != nil {
if err := c.runner.Run(ctx, c.nginxExecutable, "-s", "reload"); err != nil {
return errors.Join(
fmt.Errorf("reload host Nginx service %s: %w", c.nginxServiceName, err),
fmt.Errorf("reload host Nginx: %w", err),
c.restoreAfterFailure(ctx, rollback),
)
}
return nil
}
// restoreAfterFailure 在失败后把配置回滚为 content,并再次校验与重载 Nginx,
// 将校验与重载的错误合并返回。
func (c *Controller) restoreAfterFailure(ctx context.Context, content []byte) error {
if err := c.atomicWrite(content); err != nil {
return fmt.Errorf("restore host Nginx configuration after failure: %w", err)
}
validateErr := c.runner.Run(ctx, c.nginxExecutable, "-t")
reloadErr := c.runner.Run(ctx, c.systemctlPath, "reload", "--", c.nginxServiceName)
reloadErr := c.runner.Run(ctx, c.nginxExecutable, "-s", "reload")
return errors.Join(
wrapError("validate restored host Nginx configuration", validateErr),
wrapError("reload restored host Nginx configuration", reloadErr),
)
}
// atomicWrite 通过同目录临时文件加 rename 的方式原子替换配置文件,并保留原文件权限、
// 同步临时文件与父目录,确保替换持久且不会留下半成品。
func (c *Controller) atomicWrite(content []byte) error {
info, err := os.Lstat(c.configPath)
if err != nil {
@@ -206,12 +215,15 @@ func (c *Controller) atomicWrite(content []byte) error {
return syncDirectory(parent)
}
// commandRunner 抽象外部命令执行,便于在测试中注入记录型运行器。
type commandRunner interface {
Run(context.Context, string, ...string) error
}
// execRunner commandRunner 的生产实现,通过 os/exec 执行真实外部命令。
type execRunner struct{}
// Run 执行 executable 及其参数,失败时把命令的标准输出与错误输出附加到错误信息中。
func (execRunner) Run(ctx context.Context, executable string, arguments ...string) error {
output, err := exec.CommandContext(ctx, executable, arguments...).CombinedOutput()
if err == nil {
@@ -224,6 +236,7 @@ func (execRunner) Run(ctx context.Context, executable string, arguments ...strin
return fmt.Errorf("%w: %s", err, detail)
}
// wrapError 在 err 非空时为其附加 message 前缀并返回,err 为空则返回 nil。
func wrapError(message string, err error) error {
if err == nil {
return nil
@@ -231,6 +244,8 @@ func wrapError(message string, err error) error {
return fmt.Errorf("%s: %w", message, err)
}
// syncDirectory 打开 directory 指向的目录并调用 Sync 将其刷入磁盘,
// 保证配置目录项变更持久化。
func syncDirectory(directory string) error {
file, err := os.Open(directory)
if err != nil {