# Temporal Worker — 后续改造 TODO

本文档汇总 worker 在"重启 / 升级"场景下还可以做的改造。
已完成项不在此列；当前已完成：

- ✅ DSL 图环检测（`dsl-workflow.ts` `executeGraph` 加迭代上限）
- ✅ Outbox Poller 事务边界（每行一个事务 + 确定性 workflowId 幂等保护）
- ✅ Graceful Shutdown（`worker.shutdown()` + `outboxPoller.stop()` + health server）
- ✅ Outbox 重试时 t_workflow_instance 残留行 —— `WorkflowInstance` 加 `sourceOutboxId`，`(workflow_id, source_outbox_id)` 复合 UNIQUE，`matchAndTrigger` 改为 `INSERT ... ON CONFLICT DO UPDATE`
- ✅ DSL 提交时 DAG 校验（`dsl-validate.util.ts` + 在 `WorkflowService.publish()` 调用，循环图 / 缺 trigger / 重复 id / 悬空 edge 全部 400）
- ✅ Activity Timeout 分级（`httpActs` / `dbActs` / `notifyActs` / `codeActs` / `reportActs` 各自独立 timeout & retry policy）

---

## P0 — Activity 缺少 Heartbeat 与 Cancellation 响应

**位置**：`src/activities/http-request.ts`、`db-query.ts`、`db-execute.ts`、`code-execute.ts`

**问题**：Activity 不调 `Context.current().heartbeat()`，也不监听 `Context.current().cancellationSignal`。
Worker 收到 SIGTERM 后 `worker.shutdown()` 会向 in-flight Activity 发 cancel，但当前实现完全不响应，
必须等 `startToCloseTimeout: 60s` 自然超时。极端场景（HTTP 卡住、code-execute vm timeout 失效）
会一直占用 worker 进程到 grace period 结束被 SIGKILL，Temporal 端再等心跳超时才能重派。

**改造示例（http-request）**：

```ts
import { Context } from '@temporalio/activity';

export async function httpRequest(input: HttpRequestInput): Promise<HttpRequestOutput> {
  const ctx = Context.current();
  const controller = new AbortController();
  ctx.cancellationSignal.addEventListener('abort', () => controller.abort());

  const heartbeatTimer = setInterval(() => ctx.heartbeat(), 5_000);
  try {
    const response = await axios({ ...axiosConfig, signal: controller.signal });
    return { status: response.status, data: response.data, headers: response.headers as any };
  } finally {
    clearInterval(heartbeatTimer);
  }
}
```

**db-query / db-execute**：把 PG client 的 query 包成可取消（pg 8.x 支持 `client.cancel()`），
并在每条长查询前 `ctx.heartbeat()`。

**code-execute**：vm 不支持外部中断，至少应在 `runOnceForEach` 每个 item 之间检查 `ctx.cancellationSignal.aborted` 并提前 return。

---

## P1 — Worker Build IDs (Workflow Versioning)

**问题**：`src/index.ts` 的 `Worker.create({...})` 没有传 `buildId`。
所有版本的 worker 都从同一 task queue 拉 task，包括老 workflow 的 history replay。
`dsl-workflow.ts` 是动态解释器，但解释器自身的代码改动也会破坏 determinism：

- 改 BFS 调度顺序（`queue.shift`）→ 老 workflow replay 失败
- 改 `for_each` 的 batch concurrency 处理 → 老 workflow replay 失败
- 改 `parallel` 的 `Promise.all` vs `Promise.race` → 老 workflow replay 失败

**改造**：

```ts
const worker = await Worker.create({
  connection,
  namespace: config.temporal.namespace,
  taskQueue: config.temporal.taskQueue,
  workflowsPath: require.resolve('./workflows/dsl-workflow'),
  activities,
  buildId: process.env.WORKER_BUILD_ID || require('../package.json').version,
  useVersioning: true,
});
```

部署时还需通过 Temporal CLI 把新 buildId 标记为 default：

```bash
temporal task-queue versioning add-new-default \
  --task-queue workflow-execution \
  --build-id v1.2.0
```

> Temporal 1.21+ 支持 Worker Build IDs；1.24+ 有更易用的 Worker Deployment API，可一并评估。

---

## P1 — `for_each` 节点不响应 Cancel

**位置**：`src/workflows/dsl-workflow.ts` 的 `for_each` 分支（约 L319-355）

**问题**：item 数量很大时（比如 1000 条），`for (let i = 0; i < items.length; i += concurrency)`
循环中途用户/系统 cancel 不会立即退出，必须等循环跑完。

**改造**：每个 batch 前检查 cancel 标志：

```ts
for (let i = 0; i < items.length; i += concurrency) {
  if (isCancelled()) throw new CancellationScope().cancel;   // ← 加这一行
  const batch = items.slice(i, i + concurrency);
  // ...
}
```

---

## P2 — Activity Timeout 业务级覆盖

**当前状态**：dsl-workflow.ts 已按职责类别拆出 `httpActs` / `dbActs` / `notifyActs` / `codeActs` / `reportActs`，
各自 timeout 和 retry 独立。但仍是**类别级**配置，业务在 DSL 里指定 `input.timeout` 只透传到 axios/vm 内部，
没法影响 Temporal Activity 的 `startToCloseTimeout`。

**进一步改造**：通过 `Context.current().heartbeat()` + 业务级 timeout 透传，让 DSL 的 `config.timeoutSec`
也能影响 Activity 调度层。例如：

```ts
case 'http_request': {
  const proxyOpts = config.timeoutSec
    ? { startToCloseTimeout: `${config.timeoutSec}s`, retry: { maximumAttempts: 3 } }
    : undefined;
  const httpProxy = proxyOpts
    ? proxyActivities<...>({ ...proxyOpts })
    : httpActs;
  return { output: await httpProxy.httpRequest(config as any) };
}
```

**注意**：Workflow 内 `proxyActivities` 是确定性配置，运行时动态创建会破坏 replay。
更稳妥的做法是预先创建 N 个 timeout 档（如 30s / 120s / 600s），DSL 指定档位而非任意数值。

---

## P3 — 进程级兜底 + Health Server 强断

**位置**：`src/index.ts`

```ts
// 缺崩溃日志兜底
process.on('uncaughtException', (err) => {
  console.error('[Worker] uncaughtException:', err);
  void shutdown('uncaughtException');
  setTimeout(() => process.exit(1), 30_000).unref();
});
process.on('unhandledRejection', (reason) => {
  console.error('[Worker] unhandledRejection:', reason);
});

// healthServer.close() 不会主动断 keep-alive 连接，K8s probe 长连接会拖慢退出
await new Promise<void>((resolve) => {
  healthServer.close(() => resolve());
  healthServer.closeAllConnections?.();   // Node 18.2+
});
```
