From cf1ec7b1c6fc37914a0a48d8776a878d2b58a146 Mon Sep 17 00:00:00 2001 From: Jerry Yan <792602257@qq.com> Date: Wed, 6 May 2026 11:40:34 +0800 Subject: [PATCH] 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. --- internal/app/app.go | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/internal/app/app.go b/internal/app/app.go index 32f4b63..b100522 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -418,8 +418,13 @@ func (a *App) Run(ctx context.Context) error { a.jobs.Stop(stopCtx) stopCancel() } - // server 已停止接收新请求;等异步通知投递结束再关 pool - if err := a.dispatcher.Shutdown(shutCtx); err != nil { + // server 已停止接收新请求;等异步通知投递结束再关 pool。 + // 用独立的 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.pool.Close()