# 进行中的工作与启动确认

> 测试关闭期间进行中的请求：用启动确认（响应头或探测路由）证明 SIGTERM 到达时确实有工作在运行。

Source: https://shutdown.jscrate.dev/zh/docs/in-flight-work
Last updated: 2026-09-23

要测试关闭期间进行中的请求，shutdown-check 必须确认请求仍在进行。启动确认（start barrier）就是用来提供这个证明的。只有启动确认通过后，才会发送信号。

## 为什么需要启动确认？

如果没有启动确认，一个很快的请求可能在 `SIGTERM` 之前就已结束。这时即使服务根本没有关闭处理函数，测试也会通过，因为已经没有需要排空的工作了。

shutdown-check 会防止这种误判。如果无法证明有工作正在进行，它会返回 [SC111](https://shutdown.jscrate.dev/zh/docs/codes/sc111)，并且不发送信号。

## 选择启动确认方式

| 方式               | 通过条件                             | 适用场景                           | 请求数 |
| ------------------ | ------------------------------------ | ---------------------------------- | ------ |
| `response-headers` | 每个响应都已发送响应头，且响应体未结束 | 路由可以流式输出或延迟发送响应体   | 1–20   |
| `probe`            | 一个单独的路由从非活跃变为活跃       | 处理函数在操作完成前不发送任何内容 | 1      |

`response-headers` 证明的是有一个 HTTP 响应处于打开状态。探测路由则可以证明某个具体的内部操作已经开始。

## 使用 response-headers 启动确认

配置测试请求：

```json title="shutdown-check.json"
{
  "workload": {
    "path": "/slow",
    "concurrent": 2,
    "started": {
      "type": "response-headers",
      "timeoutMs": 5000
    }
  }
}
```

每个请求都必须在 `timeoutMs` 内收到响应头，且响应体必须仍未结束。有两个请求时，两个都必须满足启动确认。

### 编写合适的路由

Node.js 可能会把响应头留到第一次写入响应体时才发送。调用 `flushHeaders()` 可以立即发送：

```js title="server.js"
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` 之前没有收到响应头
- 路由拒绝或重置了连接

成功时，时间线中会出现：

```text
  +  114 ms  work confirmed active — 2 response(s) sent headers; bodies still in progress
```

## 使用 probe 启动确认

如果处理函数要先算出完整结果才发送响应，就使用探测路由。探测路由会报告这个操作当前是否正在运行。

```json title="shutdown-check.json"
{
  "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 }
}
```

探测分两个阶段：

1. 发出测试请求之前，探测路由必须返回 `inactiveStatus`。
2. 测试请求开始后，在它的响应仍未结束时，探测路由必须返回 `activeStatus`。

第一阶段失败时，结果为 [SC110](https://shutdown.jscrate.dev/zh/docs/codes/sc110)。第二阶段超时，或测试请求先结束时，结果为 [SC111](https://shutdown.jscrate.dev/zh/docs/codes/sc111)。

### 编写探测路由

用一个计数器跟踪真实的操作。只在测试环境中开放这个路由。

```js title="server.js"
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` 中复原，因此无论操作成功还是失败，探测路由都能如实反映。

### 解读通过时的探测时间线

```text
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` 设为只有工作成功后才会出现的文本：

```json
{
  "bodyIncludes": "report ready"
}
```

匹配区分大小写。找不到该文本时返回 [SC203](https://shutdown.jscrate.dev/zh/docs/codes/sc203)。请选择固定不变的文本，不要用生成的 ID 或时间戳。

## 安全地设计测试专用路由

- 只通过测试环境变量启用这些路由。
- 不要在生产环境中暴露内部状态。
- 避免真实扣费、发送邮件或破坏性的副作用。
- 在 `finally` 中重置计数器。
- 在较慢的 CI 机器上，也要保证测试请求的行为稳定可预期。
- 为并行运行的测试分配相互隔离的端口和状态。

## 修复常见的启动确认失败

| 现象                   | 可能原因                           | 修复方法                                   |
| ---------------------- | ---------------------------------- | ------------------------------------------ |
| 响应立即结束           | 路由太快                           | 延迟或流式输出这个受控的测试响应           |
| 始终收不到响应头       | Node.js 缓冲了响应头               | 调用 `flushHeaders()` 或写入一段响应体     |
| 探测一开始就是活跃状态 | 有残留的旧工作，或探测状态不正确   | 重置状态，并先确认返回的是 `inactiveStatus` |
| 探测始终不变为活跃     | 计数器更新太晚，或路径不对         | 在操作开始前设置状态，并检查路由           |
| 本地通过但 CI 失败     | 时间窗口太小                       | 让测试请求的耗时明显更长                   |
| 多个请求导致配置被拒绝 | 探测只支持一个请求                 | 需要并发时改用 `response-headers`          |

## 相关内容

- [测试请求与启动确认配置](https://shutdown.jscrate.dev/zh/docs/configuration#start-barrier)
- [SC110：探测路由一开始就处于活跃状态](https://shutdown.jscrate.dev/zh/docs/codes/sc110)
- [SC111：测试请求始终未处于进行中](https://shutdown.jscrate.dev/zh/docs/codes/sc111)
- [SC203：进行中请求的响应体错误](https://shutdown.jscrate.dev/zh/docs/codes/sc203)
- [测试是如何运行的](https://shutdown.jscrate.dev/zh/docs/how-it-works)
