要测试关闭期间进行中的请求,shutdown-check 必须确认请求仍在进行。启动确认(start barrier)就是用来提供这个证明的。只有启动确认通过后,才会发送信号。
为什么需要启动确认?
如果没有启动确认,一个很快的请求可能在 SIGTERM 之前就已结束。这时即使服务根本没有关闭处理函数,测试也会通过,因为已经没有需要排空的工作了。
shutdown-check 会防止这种误判。如果无法证明有工作正在进行,它会返回 SC111,并且不发送信号。
选择启动确认方式
| 方式 | 通过条件 | 适用场景 | 请求数 |
|---|---|---|---|
response-headers | 每个响应都已发送响应头,且响应体未结束 | 路由可以流式输出或延迟发送响应体 | 1–20 |
probe | 一个单独的路由从非活跃变为活跃 | 处理函数在操作完成前不发送任何内容 | 1 |
response-headers 证明的是有一个 HTTP 响应处于打开状态。探测路由则可以证明某个具体的内部操作已经开始。
使用 response-headers 启动确认
配置测试请求:
{
"workload": {
"path": "/slow",
"concurrent": 2,
"started": {
"type": "response-headers",
"timeoutMs": 5000
}
}
}每个请求都必须在 timeoutMs 内收到响应头,且响应体必须仍未结束。有两个请求时,两个都必须满足启动确认。
编写合适的路由
Node.js 可能会把响应头留到第一次写入响应体时才发送。调用 flushHeaders() 可以立即发送:
if (request.url === "/slow") {
response.writeHead(200, { "content-type": "text/plain" });
response.flushHeaders();
setTimeout(() => {
response.end("work complete\n");
}, 2000);
return;
}延迟时间必须足够长,让 shutdown-check 来得及发送信号,但要比 shutdown.deadlineMs 短。这个路由应该可以安全地重复执行,且不应修改生产数据。
了解失败情况
以下情况会导致启动确认失败:
- 在所有响应头得到确认之前,某个响应已经结束
- 并发的一组请求中,有一个提前结束
timeoutMs之前没有收到响应头- 路由拒绝或重置了连接
成功时,时间线中会出现:
+ 114 ms work confirmed active — 2 response(s) sent headers; bodies still in progress使用 probe 启动确认
如果处理函数要先算出完整结果才发送响应,就使用探测路由。探测路由会报告这个操作当前是否正在运行。
{
"command": ["node", "server.js"],
"env": { "ENABLE_TEST_ROUTES": "1" },
"baseUrl": "http://127.0.0.1:3000",
"readiness": { "path": "/health" },
"workload": {
"path": "/reports",
"method": "POST",
"status": 200,
"bodyIncludes": "report ready",
"started": {
"type": "probe",
"path": "/test/work-active",
"inactiveStatus": 204,
"activeStatus": 200,
"timeoutMs": 5000,
"intervalMs": 50
}
},
"shutdown": { "deadlineMs": 10000 }
}探测分两个阶段:
- 发出测试请求之前,探测路由必须返回
inactiveStatus。 - 测试请求开始后,在它的响应仍未结束时,探测路由必须返回
activeStatus。
第一阶段失败时,结果为 SC110。第二阶段超时,或测试请求先结束时,结果为 SC111。
编写探测路由
用一个计数器跟踪真实的操作。只在测试环境中开放这个路由。
const http = require("node:http");
const { setTimeout: delay } = require("node:timers/promises");
let activeReports = 0;
async function buildReport() {
await delay(2000);
return { rows: 42 };
}
const server = http.createServer(async (request, response) => {
if (request.url === "/health") {
response.writeHead(200).end("ok");
return;
}
if (
request.url === "/test/work-active" &&
process.env.ENABLE_TEST_ROUTES === "1"
) {
response.writeHead(activeReports > 0 ? 200 : 204).end();
return;
}
if (request.url === "/reports" && request.method === "POST") {
activeReports++;
try {
const report = await buildReport();
response.writeHead(200, { "content-type": "application/json" });
response.end(JSON.stringify({ status: "report ready", ...report }));
} finally {
activeReports--;
}
return;
}
response.writeHead(404).end();
});
server.listen(3000, "127.0.0.1");
process.on("SIGTERM", () => server.close());计数器在耗时操作开始前增加,并在 finally 中复原,因此无论操作成功还是失败,探测路由都能如实反映。
解读通过时的探测时间线
PASS SC000: Graceful shutdown verified
Timeline:
+ 6 ms process launched — pid=73999
+ 115 ms service ready — HTTP 200
+ 116 ms work request sent — #1 POST /reports
+ 117 ms work confirmed active — probe /test/work-active returned HTTP 200
+ 117 ms signal sent — SIGTERM
+ 2120 ms work request finished — #1 HTTP 200
+ 2122 ms process exited — code=0, signal=none
+ 2123 ms shutdown verified — work completed and service exited before deadline证明操作已经完成
状态码为 200 的响应,内容仍可能是错误信息。把 bodyIncludes 设为只有工作成功后才会出现的文本:
{
"bodyIncludes": "report ready"
}匹配区分大小写。找不到该文本时返回 SC203。请选择固定不变的文本,不要用生成的 ID 或时间戳。
安全地设计测试专用路由
- 只通过测试环境变量启用这些路由。
- 不要在生产环境中暴露内部状态。
- 避免真实扣费、发送邮件或破坏性的副作用。
- 在
finally中重置计数器。 - 在较慢的 CI 机器上,也要保证测试请求的行为稳定可预期。
- 为并行运行的测试分配相互隔离的端口和状态。
修复常见的启动确认失败
| 现象 | 可能原因 | 修复方法 |
|---|---|---|
| 响应立即结束 | 路由太快 | 延迟或流式输出这个受控的测试响应 |
| 始终收不到响应头 | Node.js 缓冲了响应头 | 调用 flushHeaders() 或写入一段响应体 |
| 探测一开始就是活跃状态 | 有残留的旧工作,或探测状态不正确 | 重置状态,并先确认返回的是 inactiveStatus |
| 探测始终不变为活跃 | 计数器更新太晚,或路径不对 | 在操作开始前设置状态,并检查路由 |
| 本地通过但 CI 失败 | 时间窗口太小 | 让测试请求的耗时明显更长 |
| 多个请求导致配置被拒绝 | 探测只支持一个请求 | 需要并发时改用 response-headers |