From 27f47706234afdd054742cfd382b34ee295dec53 Mon Sep 17 00:00:00 2001 From: qian Date: Sat, 15 Aug 2026 00:38:11 +0800 Subject: [PATCH] =?UTF-8?q?fix(update):=E4=BF=AE=E5=A4=8D=E5=8D=87?= =?UTF-8?q?=E7=BA=A7=E5=9B=9E=E6=BB=9A=E6=9C=AA=E7=AD=89=E5=BE=85=E4=BF=A1?= =?UTF-8?q?=E5=8F=B7=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/cmd/cmd.go | 3 +- internal/logic/update/graceful_exit.go | 48 +++++++++++++++++++ internal/logic/update/graceful_exit_unix.go | 12 +++++ .../logic/update/graceful_exit_windows.go | 8 ++++ internal/logic/update/replacer.go | 7 +-- internal/logic/update/rollback.go | 12 ++--- 6 files changed, 78 insertions(+), 12 deletions(-) create mode 100644 internal/logic/update/graceful_exit.go create mode 100644 internal/logic/update/graceful_exit_unix.go create mode 100644 internal/logic/update/graceful_exit_windows.go diff --git a/internal/cmd/cmd.go b/internal/cmd/cmd.go index e976bf7..e70737f 100644 --- a/internal/cmd/cmd.go +++ b/internal/cmd/cmd.go @@ -285,9 +285,10 @@ var ( // 1) 先排空任务池(StopSyncImageWorkers / StopAsyncPolling)——它们收尾时会写 // 用量日志 / 审计 / 结算;此时异步 Writer 必须仍存活。 // 2) 再 flush 关闭异步 Writer。 - // 3) 最后关 webhook / plugin。 + // 3) 最后关 webhook / 调度后台任务 / plugin。 // 因此把两个 task.Stop* 注册在最后(最先执行),Writer 关闭注册在前(后执行)。 defer plugin.Shutdown(ctx) + defer dispatchadapter.Shutdown() defer tenant.ShutdownWebhookDispatcher() defer common.CloseAuditLogWriter() defer common.CloseChannelErrorWriter() diff --git a/internal/logic/update/graceful_exit.go b/internal/logic/update/graceful_exit.go new file mode 100644 index 0000000..326df32 --- /dev/null +++ b/internal/logic/update/graceful_exit.go @@ -0,0 +1,48 @@ +package update + +import ( + "context" + "os" + "time" + + "github.com/gogf/gf/v2/frame/g" +) + +const ( + // exitResponseGrace 让 HTTP 响应先写出的等待时间 + exitResponseGrace = 500 * time.Millisecond + // gracefulExitTimeout 优雅关闭链路的最长等待。需覆盖 cmd.go defer 链中 + // sync-image 任务池的收尾等待(3 分钟),再留余量;超时则强制退出兜底, + // 防止个别长连接把退出拖到无限长。 + gracefulExitTimeout = 4 * time.Minute +) + +// gracefulExit 更新/回滚完成后的进程退出入口。 +// +// 不能直接 os.Exit:那会跳过 cmd.go 中 s.Run() 之后的 defer 链(排空任务池、 +// flush 用量/审计异步 Writer、退款在途任务),导致计费数据丢失。 +// 正确做法是向自身发送 SIGTERM,走 GoFrame 的信号处理链路: +// +// SIGTERM → ghttp 关闭监听并等待在途请求 → s.Run() 返回 +// → cmd.go defer 链执行(LIFO)→ main 返回,进程自然退出 +// +// 由进程管理器(systemd Restart=always / supervisor 等)随后拉起新二进制。 +// Windows 无自信号机制(且在线更新仅支持 Linux),退化为直接退出。 +func gracefulExit(ctx context.Context, reason string) { + go func() { + time.Sleep(exitResponseGrace) + g.Log().Infof(ctx, "%s: requesting graceful shutdown", reason) + + if !signalSelfTerm() { + // 平台不支持自信号:无优雅链路可用,直接退出(不阻塞调用方) + os.Exit(0) + } + + // 优雅链路正常情况下会让 main 自然返回;若超过上限仍未退出 + // (如 SSE 流长时间不结束),强制退出兜底 + time.Sleep(gracefulExitTimeout) + g.Log().Warningf(ctx, "%s: graceful shutdown not finished within %v, forcing exit", + reason, gracefulExitTimeout) + os.Exit(0) + }() +} diff --git a/internal/logic/update/graceful_exit_unix.go b/internal/logic/update/graceful_exit_unix.go new file mode 100644 index 0000000..21959e4 --- /dev/null +++ b/internal/logic/update/graceful_exit_unix.go @@ -0,0 +1,12 @@ +//go:build !windows + +package update + +import ( + "syscall" +) + +// signalSelfTerm 向自身发送 SIGTERM,触发 GoFrame 的优雅关闭信号处理 +func signalSelfTerm() bool { + return syscall.Kill(syscall.Getpid(), syscall.SIGTERM) == nil +} diff --git a/internal/logic/update/graceful_exit_windows.go b/internal/logic/update/graceful_exit_windows.go new file mode 100644 index 0000000..41abd2b --- /dev/null +++ b/internal/logic/update/graceful_exit_windows.go @@ -0,0 +1,8 @@ +//go:build windows + +package update + +// signalSelfTerm Windows 无自信号机制,返回 false 由调用方直接退出 +func signalSelfTerm() bool { + return false +} diff --git a/internal/logic/update/replacer.go b/internal/logic/update/replacer.go index 34449ad..0f6b6ac 100644 --- a/internal/logic/update/replacer.go +++ b/internal/logic/update/replacer.go @@ -153,10 +153,11 @@ func performUpdate(ctx context.Context, targetVersion, downloadURL, checksumURL // Step 8: Restart manager.setProgress(PhaseRestarting, "正在重启服务...", 90) - g.Log().Info(ctx, "Update complete, exiting for restart...") - time.Sleep(500 * time.Millisecond) // give status API a moment to respond + g.Log().Info(ctx, "Update complete, restarting...") - os.Exit(0) + // 走 SIGTERM 优雅关闭链路(见 gracefulExit 注释),让 cmd.go 的 defer 链 + // 排空任务池并 flush 用量/审计日志后自然退出,由进程管理器拉起新版本 + gracefulExit(ctx, "update complete") } // extractBinary extracts the team-api binary from a tar.gz archive to outputPath. diff --git a/internal/logic/update/rollback.go b/internal/logic/update/rollback.go index 9d02850..c6945e6 100644 --- a/internal/logic/update/rollback.go +++ b/internal/logic/update/rollback.go @@ -6,7 +6,6 @@ import ( "fmt" "os" "path/filepath" - "time" "github.com/gogf/gf/v2/frame/g" @@ -70,13 +69,10 @@ func Rollback(ctx context.Context) error { g.Log().Infof(ctx, "Rollback to %s complete, scheduling restart...", info.BackupVersion) - // 延迟退出:Rollback 在 HTTP handler 调用栈内同步执行,直接 os.Exit 会切断 - // 尚未写出的响应(前端拿到连接重置而非成功提示)。先让 handler 返回响应, - // 稍后退出进程,由进程管理器(systemd/supervisor 等)拉起旧版本 - go func() { - time.Sleep(500 * time.Millisecond) - os.Exit(0) - }() + // 走 SIGTERM 优雅退出(见 gracefulExit 注释):先让 handler 把"回滚已启动" + // 响应写出去,再触发优雅关闭链路排空任务池/异步 Writer 后自然退出, + // 由进程管理器(systemd/supervisor 等)拉起旧版本 + gracefulExit(ctx, "rollback complete") return nil }