Sails 应用优雅关闭指南:sails.lower() 方法深度解析
【免费下载链接】sailsRealtime MVC Framework for Node.js项目地址: https://gitcode.com/gh_mirrors/sa/sails
lower()是 Sails 生命周期中与lift()对应的逆操作:它会关闭已启动的应用,使其不再监听、也不再响应任何新的请求。无论是生产环境的平滑下线、测试框架中的反复启动/停止,还是程序化控制 Sails 应用,lower()都是确保资源被正确释放的关键 API。读完本文,你将掌握sails.lower()的调用方式、回调契约、底层关闭流程(WebSocket → HTTP → 事件监听器),以及它在源码和测试中的实际实现依据。
一、sails.lower()是什么
lower()用于关闭一个已 lift 的 Sails 应用,使其停止监听并停止响应任何未来的请求。它由 Sails 应用实例 提供,是官方公开 API(@api public)之一。
在 lib/app/Sails.js 中,lower与lift一起被挂载到Sails.prototype上:
Sails.prototype.lift = require('./lift'); Sails.prototype.lower = require('./lower');同时,构造函数会为该方法绑定this上下文(lib/app/Sails.js),并继承自 Node.js 的EventEmitter(lib/app/Sails.js),这为lower事件机制提供了基础。
从生命周期来看:
sails.load():加载配置、hooks、模型、路由等,但不启动服务器;sails.lift():加载并初始化应用,启动 HTTP/WebSocket 服务器,并绑定进程信号监听器;sails.lower():完成与lift()相反的工作——关闭服务器、终止子进程、移除所有事件监听器。
二、API 签名与参数说明
官方文档定义的语法如下:
sails.lower(callback);参数表
| 序号 | 参数 | 类型 | 说明 |
|---|---|---|---|
| 1 | callback | ((function?)) | 可选。在 lower 完成(或出错)时被调用的函数 |
Callback 参数
| 序号 | 参数 | 类型 | 说明 |
|---|---|---|---|
| 1 | err | ((Error?)) | 若 lower 过程中发生致命错误,错误实例将作为回调的第一个参数传入 |
源码中对这两个"可选"做了完整的容错处理(lib/app/lower.js):
- 若第一个参数是函数,则将其视为
cb,options置为undefined; - 若未提供
cb,则使用默认回调——出错时调用sails.log.error(err)记录日志,否则静默; options默认被归一为空对象,且options.delay默认值为100(毫秒)。
也就是说,sails.lower()、sails.lower(cb)、sails.lower(options, cb)三种形式都是合法的。
可用的 options(源码级)
虽然文档只公开了callback参数,但从 lib/app/lower.js 与 lib/app/lower.js 的实现可以看到,lower()还接受一个可选的options对象:
options.delay:默认100。优雅关闭模式下,HTTP 服务器先停止接受新连接,等待delay毫秒让存量连接自然结束,超时后再强制destroy;options.hardShutdown:默认false。若为true,则立即调用sails.hooks.http.destroy()切断所有连接,不做优雅等待。
这两个选项在需要精细控制下线节奏的部署场景(如负载均衡器先摘除节点、再等存量请求跑完)中非常实用。
三、完整使用示例
文档给出的标准用法如下:
sailsApp.lower( function (err) { if (err) { return console.log("Error occurred lowering Sails app: ", err); } console.log("Sails app lowered successfully!"); } )在实际项目中,它最常见的两种用法是:
用法一:测试框架中反复启动/停止应用(参考 test/unit/app.lower.test.js 的写法):
var Sails = require('sails'); var app = Sails(); async.series([ function(cb) { app.load(options, cb); }, app.initialize, app.lower ], cb);用法二:程序化关闭正在运行的应用(可配合进程退出):
sails.lower(function(err) { if (err) { throw err; } process.exit(0); });用法三:硬下线(立即切断所有连接):
sails.lower({ hardShutdown: true }, function(err) { if (err) { sails.log.error(err); } });四、lower()底层执行流程剖析
lower()的核心实现位于 lib/app/lower.js,其执行序列可以概括为以下五个阶段。
1. 立即置位sails._exiting标志
进入lower()后,源码第一件关键动作是(lib/app/lower.js):
sails._exiting = true;该标志供核心 hooks 与 Sails 内部使用,用于停止处理新的 HTTP 请求、避免在关闭过程中出现难看的错误信息。例如 lib/app/private/initialize.js 中注册的exit监听器会检查sails._exiting,防止重复触发 lower。
2. 执行beforeShutdown钩子
lower()会先检查sails.config.beforeShutdown(lib/app/lower.js)。若应用配置了该函数,则会先执行它,等待其回调后再继续清理流程——这是应用在关闭前做最后业务收尾(如通知外部系统、落盘状态、解绑第三方资源)的官方扩展点:
var beforeShutdown = (sails.config && sails.config.beforeShutdown) || function(cb) { return cb(); }; beforeShutdown(function(err) { // 即使 beforeShutdown 出错,也会继续完成其余清理任务 if (err) { sails.log.error(err); } // ...后续关闭流程 });3. 向所有子进程发送 SIGINT
如果应用通过sails.childProcesses跟踪了子进程(数组在 lib/app/Sails.js 中初始化),lower()会逐一调用childProcess.kill('SIGINT')通知其退出(lib/app/lower.js),并记录被杀进程的 PID。对每个子进程的 kill 均包裹在try/catch中,单个进程 kill 失败不会中断整体流程。
4. 依序关闭 Socket 服务器与 HTTP 服务器
lower()会先发出lower事件,然后通过async.series按顺序执行两个关闭任务(lib/app/lower.js):
先关闭 sockets hook 的服务器:若 sockets hook 被禁用、或 socket 服务器正与主 HTTP 服务器共享同一个底层 server(piggybacking),则跳过(避免 socket.io 关闭时再次关闭 HTTP server 导致close事件重复触发)。否则调用sails.io.close(),并设置了 100ms 的超时兜底:即使close事件迟迟不来,也强制继续。
再关闭 HTTP 服务器:若options.hardShutdown为真,直接调用sails.hooks.http.destroy()立刻摧毁服务器;否则先调用server.close()停止接收新连接,同时启动options.delay(默认 100ms)的定时器,到期后再调用destroy兜底清理残留连接。
这里的sails.hooks.http.destroy定义在 lib/hooks/http/initialize.js:它会调用server.close(done),并遍历openTcpConnections中所有尚未关闭的 TCP 连接(该表在每次connection事件时记录、close事件时清除,见 lib/hooks/http/initialize.js),逐一destroy(),从而在硬下线场景下彻底切断存量连接。
5. 清理全部事件监听器
两个服务器关闭后,lower()会做最后的资源回收(lib/app/lower.js):
- 遍历
sails._events,对每个事件名调用removeAllListeners,把应用对象上注册的监听器全部移除; - 移除初始化阶段挂到
process上的SIGUSR2、SIGINT、SIGTERM、exit四个监听器(保存于sails._processListeners,定义见 lib/app/private/initialize.js),并将该引用置空; - 若
sails.config.process.removeAllListeners被设置,则输出一条废弃警告(该配置自 v0.12 起已不推荐,官方建议逐个移除监听器)。
整个async.series的回调会把结果(或理论上出现的异常)透传给lower()的cb。
五、lower()与进程信号的联动
lower()并不是只能在代码里手动调用——Sails 在 lib/app/private/initialize.js 中为进程注册了四个信号监听器,它们都会把控制权交给lower():
SIGUSR2(如 nodemon 重启场景):sails.lower()完成后,以SIGUSR2重新 kill 自身进程;SIGINT/SIGTERM(Ctrl+C 或 kill 命令):sails.lower()完成后调用process.exit();exit:若sails._exiting尚未置位,则自动调用sails.lower()做兜底清理。
这也解释了为何 lib/app/lift.js 在 lift 失败时会调用sails.lower()来回收已初始化了一半的资源——两者在生命周期上是严格配对的。
六、测试用例:验证 lower 的资源清理能力
仓库中针对lower()的测试直接印证了它的核心契约:
- test/unit/app.lower.test.js:连续 lift/lower 15 个 Sails 应用(模拟测试环境中的反复启停),断言
SIGUSR2、SIGINT、SIGTERM、exit四个信号的监听器数量与测试前完全一致——证明lower()确实完整移除了初始化阶段添加的进程监听器,不会造成监听器泄漏。 - test/integration/lift.lower.test.js:在真实 lift 场景下重复执行同样的 15 次启停循环(指定端口 1342),验证完整生命周期下监听器同样被清理干净。
这套测试模式也是应用开发者在自己项目里编写"启动/关闭"类测试的参考样板:先记录process.listeners(...)快照,执行完一轮 lift/lower 后比对数量。
七、注意事项与最佳实践
文档原注的两点约束必须牢记:
- 应用在关闭 HTTP 与 WebSocket 服务之前,会先发出
lower事件;- 已 lower 的应用不能再次 lift。
结合源码与使用场景,补充几条实践建议:
- 不要把
lower()当普通函数重复调用:它是一次性的完整拆除流程。若应用已经 lower(sails._exiting === true),重复调用可能造成状态不一致,且 lifted 标志不会再恢复。 - 用
beforeShutdown钩子做业务收尾:如果你需要在端口关闭前完成"健康检查摘除"、消息队列解绑等动作,在sails.config.beforeShutdown中编排这些异步任务是最干净的方案。 - 区分优雅下线与硬下线:默认行为(等待
delay毫秒让存量连接自然结束)适合平滑发布;{ hardShutdown: true }适合需要立即切断一切的场景。若对停机时间敏感,可调大delay等待更长时间。 - 在测试框架里务必成对调用:参考 test/unit/app.lower.test.js 的做法,
load/lift与lower成对出现,防止事件监听器与 TCP 连接在多次测试间累积泄漏。 - 信号驱动的关闭同样走 lower 流程:在部署平台(如 Kubernetes、Docker)发送
SIGTERM时,Sails 会自动进入上述优雅关闭流程,无需在业务代码里重复实现。
若想进一步了解整个应用生命周期(load → lift → lower)以及事件触发顺序,可继续阅读仓库中的 lifecycle.md、sails.lift.md 与 Programmatic Usage;lower()的完整实现与调用链可直接查看 lib/app/lower.js、lib/app/private/initialize.js 与 lib/hooks/http/initialize.js。
【免费下载链接】sailsRealtime MVC Framework for Node.js项目地址: https://gitcode.com/gh_mirrors/sa/sails
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考