9.3 KiB
lifecycle
后台组件分阶段关闭
从 v0.15.0-preview 起,生成项目使用以下顺序:HTTP 停止接收并排空 → 全部 OnStop → 全部 OnDrain → 逆序 OnClose → 关闭日志。初始化、创建 App 或监听失败也经过后台收尾。Runtime 不导入 Orbit、数据库、ORM 或 Redis。
在项目维护的 svc.Init 中登记即可,main/runtimeGen 由生成器更新。例如使用可选的内存 task.Runner:
jobs, err := task.New(task.Config{Workers: 4, QueueSize: 128})
if err != nil { return err }
c.OnStop("jobs", jobs.StopAccepting)
c.OnDrain("jobs", jobs.Wait)
// 把 jobs 保存到项目的 svc 字段,供 Logic 使用。
// 已接受任务使用 c.WorkContext(),保留到 HTTP 和后台正常排空结束。
数据库、Redis 或第三方客户端仍用已有的 c.OnClose(client.Close);框架创建的 DB 已登记,不要再次登记。这里的 jobs 不再额外登记 Close:StopAccepting 停止定时调度/提交,Wait 等待 worker 与回调全部退出,已完成其生命周期。
任何自定义组件都可以登记 func(context.Context) error,不必实现或依赖框架任务接口:
- OnStop 只停止接收新工作,不能关闭正在执行的工作所需的资源。每个 OnStop 必须有同名 OnDrain;缺失时拒绝释放共享资源。
- OnDrain 返回 nil 表示已经收尾,包含最终写入、确认和监控协程退出。返回错误不作为已完成证明;运行中的业务错误可以先通过 RequestShutdown(err) 上报,再等完成后由 Drain 返回 nil。
- 同一阶段的回调并发执行,不按登记顺序执行;框架等待整个阶段结束后才进入下一阶段。有组件之间的先后依赖时,组合为一个 Stop/Drain 登记项,由该项明确组织内部顺序。Stop 不能等待 Drain 才会完成的工作。
- OnClose 保留原来的同步逆序执行;普通项目无后台组件时无需登记新钩子。生命周期钩子不能再次调用 Runtime.Close/Finish,否则会等待自身;可以调用 RequestShutdown。
- 所有依赖初始化成功后立即登记,再启动使用它们的工作。构造器交付前失败的内部资源仍由构造器释放。
两种 context
c.Context() / c.Lifecycle.Context() 是早期退出通知,会随外部信号或后台故障取消。c.WorkContext() 保留初始化 context 的值但不继承其取消/截止时间,允许已接受工作在 HTTP 收尾期间继续使用依赖。
停止接收工作由 OnStop 负责,不能仅等待 WorkContext 取消。正常情况下 WorkContext 在后台排空完成后取消;阶段失败或超时会请求取消工作。框架不假设取消已经意味着 goroutine 结束,仍以 Drain 的完成为准。每个业务任务的执行时限继续由项目/任务组件设置。
时限与失败
server:
shutdownTimeout: 10s # HTTP 排空时限
http:
shutdownCancelTimeout: 5s # HTTP 取消后的收尾窗口
lifecycle:
stopTimeout: 5s
drainTimeout: 10s
cancelTimeout: 5s
这些是各阶段的整体预算,不是每个回调分别等待这么久。零使用默认值,负值在启动前拒绝。环境变量仅通过配置文件显式引用,例如 cancelTimeout: "${WORK_CANCEL_TIMEOUT:-5s}",不存在自动覆盖或别名优先。后台生命周期配置应在 config 中设置,不放在创建 App 时才执行的 AppOptions 中。
阶段超时会取消阶段 context 和 WorkContext,再给尚未返回的钩子一个 cancelTimeout 收尾窗口。若所有钩子都返回 nil,可以释放依赖,但仍返回超时错误,进程非零退出。task.Wait 收到取消会取消其任务并返回错误;这不证明全部任务已退出,因此仍按以下保守规则处理。
Stop 超时且仍未返回的组件,不会再并发调用它同名的 Drain;其他组件仍可继续收尾。此时共享资源始终保留,避免未退出的 Stop 和 Drain 同时操作同一个组件。
钩子返回错误、panic、缺少匹配 Drain,或窗口结束后仍有钩子未返回时,Runtime 返回可由 errors.Is(err, lifecycle.ErrResourcesRetained) 判断的错误,跳过全部 OnClose;生成入口也保留借用的日志实例并非零退出,由进程退出释放剩余 OS 资源。不会悄悄重试 Close,也不会让晚到的后台结果触发自动关闭。错误链保留初始化/监听/后台故障及具体 HookError 的阶段、名称和原始原因。
手写入口必须处理这个错误并终止该服务,不能继续运行半关闭的 Runtime;嵌入式宿主需自行安排隔离/终止策略。Go 不能强杀不响应取消的 goroutine,有限等待与保证它已经结束不能同时无条件成立。OnClose 仍要求自身能返回;阶段时限不会强制中断资源 Close。HTTP 超时也必须经过下述完成屏障,业务处理器仍须响应请求取消。
手写项目使用 lifecycle.NewRuntimeWithConfig(ctx, cfg);完成 HTTP 排空后调用 runtime.Finish(runErr)。库本身不执行 os.Exit。
HTTP 请求与依赖的关闭屏障
App 统计已进入且尚未退出的整个处理链,包括中间件、响应编码和 defer;停止接收与登记请求共用同步屏障。关闭连接、取消 context、HTTP/2 session 结束均不等于处理链结束。
RunContext / ServeContext 先按 shutdownTimeout 排空,超时取消并关闭连接;App 随后按 http.shutdownCancelTimeout(零默认 5s)等待处理器完成收尾。收尾成功则执行逆序 OnShutdown,仍保留原排空超时错误。收尾窗口结束时还有处理器运行,则永久返回 ErrResourcesRetained 并跳过所有 OnShutdown;后续 Close 或晚到的处理器返回不会触发补做清理。
Runtime.Finish(runErr) 收到该错误后,继续执行后台 OnStop/OnDrain 并取消 WorkContext,但跳过 OnClose;后续 Runtime.Close 也保持资源保留。生成入口通过这条错误链保留数据库等共享依赖和借用日志,并非零退出。手写入口必须传递 runErr 给 Finish,并在 errors.Is(runErr, lifecycle.ErrResourcesRetained) 时跳过自己拥有的额外资源清理,不能无条件 defer DB.Close/Logger.Close。
App.Close 本身会停止接收、取消请求并等待收尾,适用于外部 HTTP 宿主;正常优雅排空应先由宿主停止监听并 Shutdown,再调用 Close。不要在请求处理器或清理钩子内部调用 Close(会等待自身)。App、Engine、NewServer().Handler 和 RunWith 都经过统计;已停止的 App 返回简单 503,不再调用可能使用已关闭依赖的中间件或编码器。App 不支持关闭后重新启动。
外部 Runner 返回不构成请求完成证明,App 仍执行自己的完成屏障;Runner 负责关闭自己拥有的 listener/连接。原始 Router().ServeHTTP 绕过 App。处理器退出后继续运行的自建 goroutine、Hijack/WebSocket 后台工作,必须用 OnStop/OnDrain 注册,框架不会推断这些工作的生命周期。HTTP/2 低层 Shutdown 也会等待独立 handler goroutine;超时返回 error 仍不表示它已退出。
新行为增加一个有限 HTTP 收尾窗口,调用方总停机预算应计入它。已有生成项目升级框架即可获得 Runtime.Finish 的保护;重新生成可同步入口注释。可运行示例见 httpSafety。
升级
v0.14.x 受管理项目将 module 和 CLI 一起升级到 v0.15.0-preview,再运行 go-omni api generate、go-omni api generate --check 和业务测试。生成器更新 main/runtimeGen,保留 svc、Logic 和配置。已有配置不添加新字段也会使用默认时限,需要调整时再编辑 yaml。
要获得后台分阶段保证,项目需把原先 OnClose(worker.Close) 的后台组件改成对应 Stop/Drain,并使用适合的工作 context;框架不会猜测旧 Close 回调负责数据库还是 worker。仅在 OnClose 中登记的旧任务继续遵循旧的逆序清理规则。
退出通知
Controller 是服务退出通知入口,不管理具体数据库、租约或后台任务。生成项目已经将它接入 svc 与 main,业务在 svc 调用 c.RequestShutdown(err) 即可,见 项目指南。
手写服务也可组合现有 App:
control := lifecycle.New(ctx)
defer control.RequestShutdown(nil)
// 在依赖的故障回调中调用,不阻塞等待排空。
control.RequestShutdown(cause)
// RunContext 会观察到已发生的退出通知,不必等监听成功才能通知。
err := app.RunContext(control.Context())
实际使用时 App 和依赖通常先初始化,再由并发监控回调请求退出。资源在 HTTP 排空完成后由调用方关闭,并读取 control.Err() 合并清理期间报告的首个故障。通知本身不会关闭资源、执行 os.Exit 或发送操作系统信号。
RequestShutdown(nil) 表示正常停止,非空 error 表示故障。Controller 保留首个非空故障,重复调用和并发调用安全。Cause(ctx) 将符合 errors.Is(err, context.Canceled) 的取消原因视为正常停止(包含新版 Go 的信号原因),保留其他取消原因和 DeadlineExceeded。显式 RequestShutdown(err) 上报的非空错误始终由 Controller.Err 保留,包括包装 context.Canceled 的错误。App.RunContext、ServeContext、RunWith 均保留这些异常原因,与网络适配器无关。