fix(app): give dispatcher.Shutdown its own ctx in ctx.Done branch

Final cross-cutting review finding: shutCtx was shared between
server.Shutdown and dispatcher.Shutdown, with jobs.Stop's wall-clock
time burning the budget in between. Under load, dispatcher.Shutdown
could receive an already-dead ctx and skip draining in-flight async
notify dispatches (including webhook enqueues). Mirror the errCh
branch by creating a fresh 10s drainCtx for dispatcher.Shutdown.
This commit is contained in:
2026-05-06 11:40:34 +08:00
parent 187991378b
commit cf1ec7b1c6
+7 -2
View File
@@ -418,8 +418,13 @@ func (a *App) Run(ctx context.Context) error {
a.jobs.Stop(stopCtx) a.jobs.Stop(stopCtx)
stopCancel() stopCancel()
} }
// server 已停止接收新请求;等异步通知投递结束再关 pool // server 已停止接收新请求;等异步通知投递结束再关 pool
if err := a.dispatcher.Shutdown(shutCtx); err != nil { // 用独立的 drainCtx:shutCtx 的 30s 预算已被 server.Shutdown 与
// jobs.Stop 的实际墙钟时间消耗,复用可能让 dispatcher 拿到已死 ctx
// 而立即返回,丢失在途的 webhook enqueue(DB 写)。
drainCtx, drainCancel := context.WithTimeout(context.Background(), 10*time.Second)
defer drainCancel()
if err := a.dispatcher.Shutdown(drainCtx); err != nil {
a.log.Warn("notify dispatcher drain", "err", err.Error()) a.log.Warn("notify dispatcher drain", "err", err.Error())
} }
a.pool.Close() a.pool.Close()