@gadmin2n/schematics 0.0.136 → 0.0.138
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/application/files/gadmin2-game-angle-demo/Dockerfile.server +2 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/compose-ctl.sh +1 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/config/prisma/canvas.prisma +1 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/config/prisma/workflow.prisma +14 -3
- package/dist/lib/application/files/gadmin2-game-angle-demo/config/ui/User.ts +4 -4
- package/dist/lib/application/files/gadmin2-game-angle-demo/docker-compose.md +2 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/docker-compose.yml +13 -92
- package/dist/lib/application/files/gadmin2-game-angle-demo/readme.md +105 -7
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/ensure-database.js +1 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/package.json +5 -5
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/prisma.config.ts +9 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/scripts/notify-dev-servers.js +9 -8
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/scripts/wait-for-nest-ready.js +5 -5
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/workflow-node-types.ts +43 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/workflow.seed.ts +9 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/workflows.ts +24 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/canvas/canvas-export.dto.ts +27 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/canvas/canvas-export.service.spec.ts +430 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/canvas/canvas-export.service.ts +203 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/canvas/canvas.controller.ts +36 -3
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/canvas/canvas.module.ts +2 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/temporalTenantConfig/temporalTenantConfig.controller.spec.ts +54 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/temporalTenantConfig/temporalTenantConfig.controller.ts +245 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/temporalTenantConfig/temporalTenantConfig.module.ts +10 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/temporalTenantConfig/temporalTenantConfig.service.spec.ts +366 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/temporalTenantConfig/temporalTenantConfig.service.ts +110 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-execution.dto.ts +19 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.controller.ts +15 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.service.spec.ts +79 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.service.ts +41 -19
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/start-prod.sh +5 -5
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/README.md +37 -573
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/ecosystem.dev.config.cjs +12 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/package.json +2 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/layout/header.tsx +3 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/config/routeRegistry.tsx +39 -26
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/i18n.ts +3 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/locales/en/common.json +3 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/locales/zh_CN/common.json +3 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/CanvasEditPage.tsx +23 -5
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/CanvasListPage.tsx +66 -10
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/CanvasPage.tsx +16 -7
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/CanvasToolbar.tsx +21 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/canvasApi.ts +30 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/components/ExportModal.tsx +84 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/components/ImportModal.tsx +166 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/hooks/useCanvasHistory.ts +52 -13
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/DslView.tsx +6 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/NodePropertyPanel.tsx +6 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/editor.tsx +17 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/instance-detail.tsx +18 -6
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/temporal-tenant/index.tsx +225 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/utilities/lazy-with-reload.ts +31 -0
- package/package.json +1 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-dsl-validate.spec.ts +0 -220
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-dsl-validate.ts +0 -132
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/TODO.md +0 -152
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/config/development-sql.yaml +0 -5
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/docker-compose.yml +0 -25
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/package.json +0 -13
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/.dockerignore +0 -12
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/.env +0 -7
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/Dockerfile +0 -79
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/GRACEFUL-DEPLOYMENT.md +0 -270
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/SANDBOX.md +0 -122
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/package-lock.json +0 -4285
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/package.json +0 -28
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/activities/code-execute.ts +0 -51
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/activities/db-execute.ts +0 -85
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/activities/db-query.ts +0 -35
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/activities/http-request.ts +0 -54
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/activities/index.ts +0 -6
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/activities/reporting.ts +0 -85
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/activities/send-notification.ts +0 -47
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/config.ts +0 -13
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/dsl/condition.ts +0 -101
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/dsl/context.ts +0 -58
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/dsl/graph.ts +0 -184
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/dsl/helpers.ts +0 -133
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/dsl/node-types.ts +0 -96
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/dsl/types.ts +0 -77
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/dsl/validate.ts +0 -109
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/index.ts +0 -101
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/outbox-poller.ts +0 -382
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/tests/activities/code-execute.test.ts +0 -44
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/tests/activities/http-request.test.ts +0 -87
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/tests/cron-trigger-workflow.test.ts +0 -20
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/tests/helpers.test.ts +0 -225
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/tests/node-type-consistency.test.ts +0 -101
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/tests/validate.test.ts +0 -205
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/workflows/dsl-workflow.ts +0 -499
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/tsconfig.json +0 -19
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/vitest.config.ts +0 -8
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/yarn.lock +0 -1905
|
@@ -1,152 +0,0 @@
|
|
|
1
|
-
# Temporal Worker — 后续改造 TODO
|
|
2
|
-
|
|
3
|
-
本文档汇总 worker 在"重启 / 升级"场景下还可以做的改造。
|
|
4
|
-
已完成项不在此列;当前已完成:
|
|
5
|
-
|
|
6
|
-
- ✅ DSL 图环检测(`dsl-workflow.ts` `executeGraph` 加迭代上限)
|
|
7
|
-
- ✅ Outbox Poller 事务边界(每行一个事务 + 确定性 workflowId 幂等保护)
|
|
8
|
-
- ✅ Graceful Shutdown(`worker.shutdown()` + `outboxPoller.stop()` + health server)
|
|
9
|
-
- ✅ Outbox 重试时 t_workflow_instance 残留行 —— `WorkflowInstance` 加 `sourceOutboxId`,`(workflow_id, source_outbox_id)` 复合 UNIQUE,`matchAndTrigger` 改为 `INSERT ... ON CONFLICT DO UPDATE`
|
|
10
|
-
- ✅ DSL 提交时 DAG 校验(`dsl-validate.util.ts` + 在 `WorkflowService.publish()` 调用,循环图 / 缺 trigger / 重复 id / 悬空 edge 全部 400)
|
|
11
|
-
- ✅ Activity Timeout 分级(`httpActs` / `dbActs` / `notifyActs` / `codeActs` / `reportActs` 各自独立 timeout & retry policy)
|
|
12
|
-
|
|
13
|
-
---
|
|
14
|
-
|
|
15
|
-
## P0 — Activity 缺少 Heartbeat 与 Cancellation 响应
|
|
16
|
-
|
|
17
|
-
**位置**:`src/activities/http-request.ts`、`db-query.ts`、`db-execute.ts`、`code-execute.ts`
|
|
18
|
-
|
|
19
|
-
**问题**:Activity 不调 `Context.current().heartbeat()`,也不监听 `Context.current().cancellationSignal`。
|
|
20
|
-
Worker 收到 SIGTERM 后 `worker.shutdown()` 会向 in-flight Activity 发 cancel,但当前实现完全不响应,
|
|
21
|
-
必须等 `startToCloseTimeout: 60s` 自然超时。极端场景(HTTP 卡住、code-execute vm timeout 失效)
|
|
22
|
-
会一直占用 worker 进程到 grace period 结束被 SIGKILL,Temporal 端再等心跳超时才能重派。
|
|
23
|
-
|
|
24
|
-
**改造示例(http-request)**:
|
|
25
|
-
|
|
26
|
-
```ts
|
|
27
|
-
import { Context } from '@temporalio/activity';
|
|
28
|
-
|
|
29
|
-
export async function httpRequest(input: HttpRequestInput): Promise<HttpRequestOutput> {
|
|
30
|
-
const ctx = Context.current();
|
|
31
|
-
const controller = new AbortController();
|
|
32
|
-
ctx.cancellationSignal.addEventListener('abort', () => controller.abort());
|
|
33
|
-
|
|
34
|
-
const heartbeatTimer = setInterval(() => ctx.heartbeat(), 5_000);
|
|
35
|
-
try {
|
|
36
|
-
const response = await axios({ ...axiosConfig, signal: controller.signal });
|
|
37
|
-
return { status: response.status, data: response.data, headers: response.headers as any };
|
|
38
|
-
} finally {
|
|
39
|
-
clearInterval(heartbeatTimer);
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
```
|
|
43
|
-
|
|
44
|
-
**db-query / db-execute**:把 PG client 的 query 包成可取消(pg 8.x 支持 `client.cancel()`),
|
|
45
|
-
并在每条长查询前 `ctx.heartbeat()`。
|
|
46
|
-
|
|
47
|
-
**code-execute**:vm 不支持外部中断,至少应在 `runOnceForEach` 每个 item 之间检查 `ctx.cancellationSignal.aborted` 并提前 return。
|
|
48
|
-
|
|
49
|
-
---
|
|
50
|
-
|
|
51
|
-
## P1 — Worker Build IDs (Workflow Versioning)
|
|
52
|
-
|
|
53
|
-
**问题**:`src/index.ts` 的 `Worker.create({...})` 没有传 `buildId`。
|
|
54
|
-
所有版本的 worker 都从同一 task queue 拉 task,包括老 workflow 的 history replay。
|
|
55
|
-
`dsl-workflow.ts` 是动态解释器,但解释器自身的代码改动也会破坏 determinism:
|
|
56
|
-
|
|
57
|
-
- 改 BFS 调度顺序(`queue.shift`)→ 老 workflow replay 失败
|
|
58
|
-
- 改 `for_each` 的 batch concurrency 处理 → 老 workflow replay 失败
|
|
59
|
-
- 改 `parallel` 的 `Promise.all` vs `Promise.race` → 老 workflow replay 失败
|
|
60
|
-
|
|
61
|
-
**改造**:
|
|
62
|
-
|
|
63
|
-
```ts
|
|
64
|
-
const worker = await Worker.create({
|
|
65
|
-
connection,
|
|
66
|
-
namespace: config.temporal.namespace,
|
|
67
|
-
taskQueue: config.temporal.taskQueue,
|
|
68
|
-
workflowsPath: require.resolve('./workflows/dsl-workflow'),
|
|
69
|
-
activities,
|
|
70
|
-
buildId: process.env.WORKER_BUILD_ID || require('../package.json').version,
|
|
71
|
-
useVersioning: true,
|
|
72
|
-
});
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
部署时还需通过 Temporal CLI 把新 buildId 标记为 default:
|
|
76
|
-
|
|
77
|
-
```bash
|
|
78
|
-
temporal task-queue versioning add-new-default \
|
|
79
|
-
--task-queue workflow-execution \
|
|
80
|
-
--build-id v1.2.0
|
|
81
|
-
```
|
|
82
|
-
|
|
83
|
-
> Temporal 1.21+ 支持 Worker Build IDs;1.24+ 有更易用的 Worker Deployment API,可一并评估。
|
|
84
|
-
|
|
85
|
-
---
|
|
86
|
-
|
|
87
|
-
## P1 — `for_each` 节点不响应 Cancel
|
|
88
|
-
|
|
89
|
-
**位置**:`src/workflows/dsl-workflow.ts` 的 `for_each` 分支(约 L319-355)
|
|
90
|
-
|
|
91
|
-
**问题**:item 数量很大时(比如 1000 条),`for (let i = 0; i < items.length; i += concurrency)`
|
|
92
|
-
循环中途用户/系统 cancel 不会立即退出,必须等循环跑完。
|
|
93
|
-
|
|
94
|
-
**改造**:每个 batch 前检查 cancel 标志:
|
|
95
|
-
|
|
96
|
-
```ts
|
|
97
|
-
for (let i = 0; i < items.length; i += concurrency) {
|
|
98
|
-
if (isCancelled()) throw new CancellationScope().cancel; // ← 加这一行
|
|
99
|
-
const batch = items.slice(i, i + concurrency);
|
|
100
|
-
// ...
|
|
101
|
-
}
|
|
102
|
-
```
|
|
103
|
-
|
|
104
|
-
---
|
|
105
|
-
|
|
106
|
-
## P2 — Activity Timeout 业务级覆盖
|
|
107
|
-
|
|
108
|
-
**当前状态**:dsl-workflow.ts 已按职责类别拆出 `httpActs` / `dbActs` / `notifyActs` / `codeActs` / `reportActs`,
|
|
109
|
-
各自 timeout 和 retry 独立。但仍是**类别级**配置,业务在 DSL 里指定 `input.timeout` 只透传到 axios/vm 内部,
|
|
110
|
-
没法影响 Temporal Activity 的 `startToCloseTimeout`。
|
|
111
|
-
|
|
112
|
-
**进一步改造**:通过 `Context.current().heartbeat()` + 业务级 timeout 透传,让 DSL 的 `config.timeoutSec`
|
|
113
|
-
也能影响 Activity 调度层。例如:
|
|
114
|
-
|
|
115
|
-
```ts
|
|
116
|
-
case 'http_request': {
|
|
117
|
-
const proxyOpts = config.timeoutSec
|
|
118
|
-
? { startToCloseTimeout: `${config.timeoutSec}s`, retry: { maximumAttempts: 3 } }
|
|
119
|
-
: undefined;
|
|
120
|
-
const httpProxy = proxyOpts
|
|
121
|
-
? proxyActivities<...>({ ...proxyOpts })
|
|
122
|
-
: httpActs;
|
|
123
|
-
return { output: await httpProxy.httpRequest(config as any) };
|
|
124
|
-
}
|
|
125
|
-
```
|
|
126
|
-
|
|
127
|
-
**注意**:Workflow 内 `proxyActivities` 是确定性配置,运行时动态创建会破坏 replay。
|
|
128
|
-
更稳妥的做法是预先创建 N 个 timeout 档(如 30s / 120s / 600s),DSL 指定档位而非任意数值。
|
|
129
|
-
|
|
130
|
-
---
|
|
131
|
-
|
|
132
|
-
## P3 — 进程级兜底 + Health Server 强断
|
|
133
|
-
|
|
134
|
-
**位置**:`src/index.ts`
|
|
135
|
-
|
|
136
|
-
```ts
|
|
137
|
-
// 缺崩溃日志兜底
|
|
138
|
-
process.on('uncaughtException', (err) => {
|
|
139
|
-
console.error('[Worker] uncaughtException:', err);
|
|
140
|
-
void shutdown('uncaughtException');
|
|
141
|
-
setTimeout(() => process.exit(1), 30_000).unref();
|
|
142
|
-
});
|
|
143
|
-
process.on('unhandledRejection', (reason) => {
|
|
144
|
-
console.error('[Worker] unhandledRejection:', reason);
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
// healthServer.close() 不会主动断 keep-alive 连接,K8s probe 长连接会拖慢退出
|
|
148
|
-
await new Promise<void>((resolve) => {
|
|
149
|
-
healthServer.close(() => resolve());
|
|
150
|
-
healthServer.closeAllConnections?.(); // Node 18.2+
|
|
151
|
-
});
|
|
152
|
-
```
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
services:
|
|
2
|
-
temporal:
|
|
3
|
-
image: temporalio/auto-setup:latest
|
|
4
|
-
ports:
|
|
5
|
-
- "7233:7233"
|
|
6
|
-
environment:
|
|
7
|
-
- DB=postgres12
|
|
8
|
-
- DB_PORT=5432
|
|
9
|
-
- POSTGRES_USER=kavenma
|
|
10
|
-
- POSTGRES_PWD=kavenma
|
|
11
|
-
- POSTGRES_SEEDS=host.docker.internal
|
|
12
|
-
- DYNAMIC_CONFIG_FILE_PATH=config/dynamicconfig/development-sql.yaml
|
|
13
|
-
volumes:
|
|
14
|
-
- ./config:/etc/temporal/config/dynamicconfig
|
|
15
|
-
extra_hosts:
|
|
16
|
-
- "host.docker.internal:host-gateway"
|
|
17
|
-
|
|
18
|
-
temporal-ui:
|
|
19
|
-
image: temporalio/ui:latest
|
|
20
|
-
ports:
|
|
21
|
-
- "8080:8080"
|
|
22
|
-
environment:
|
|
23
|
-
- TEMPORAL_ADDRESS=temporal:7233
|
|
24
|
-
depends_on:
|
|
25
|
-
- temporal
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "gadmin-temporal",
|
|
3
|
-
"private": true,
|
|
4
|
-
"scripts": {
|
|
5
|
-
"up": "docker compose up -d",
|
|
6
|
-
"down": "docker compose down",
|
|
7
|
-
"logs": "docker compose logs -f",
|
|
8
|
-
"worker:install": "cd worker && npm install",
|
|
9
|
-
"worker:dev": "cd worker && npm run dev",
|
|
10
|
-
"worker:build": "cd worker && npm run build",
|
|
11
|
-
"worker:start": "cd worker && npm start"
|
|
12
|
-
}
|
|
13
|
-
}
|
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
# syntax=docker/dockerfile:1
|
|
2
|
-
#
|
|
3
|
-
# Temporal Worker 镜像
|
|
4
|
-
#
|
|
5
|
-
# Build context: temporal/worker/
|
|
6
|
-
# docker build -t gadmin-workflow-worker -f temporal/worker/Dockerfile temporal/worker
|
|
7
|
-
#
|
|
8
|
-
# Worker 通过 require.resolve('./workflows/dsl-workflow') 在运行时加载已编译的
|
|
9
|
-
# workflow JS,无需 bundleWorkflowCode 预打包。
|
|
10
|
-
#
|
|
11
|
-
# 为什么用 node:20-slim 不用 node:20-alpine:
|
|
12
|
-
# @temporalio/core-bridge 的 native binding 是 glibc 二进制。alpine 用 musl libc,
|
|
13
|
-
# 会在 require 时报 "Error relocating ...: __register_atfork: symbol not found" 直接崩。
|
|
14
|
-
# slim 是 debian-bookworm-slim 基底,glibc,体积也只比 alpine 大几十 MB。
|
|
15
|
-
|
|
16
|
-
# ─── 阶段 1:构建 ────────────────────────────────────────────
|
|
17
|
-
FROM node:20-slim AS builder
|
|
18
|
-
|
|
19
|
-
WORKDIR /app
|
|
20
|
-
|
|
21
|
-
# 禁止 husky 在无 .git 环境下报错
|
|
22
|
-
ENV HUSKY=0
|
|
23
|
-
|
|
24
|
-
# 先复制依赖描述文件,源码未变时此层可复用缓存
|
|
25
|
-
COPY package.json yarn.lock ./
|
|
26
|
-
|
|
27
|
-
# 安装全量依赖(含 devDependencies,用于 TypeScript 编译)
|
|
28
|
-
RUN --mount=type=cache,target=/root/.yarn \
|
|
29
|
-
yarn install --frozen-lockfile
|
|
30
|
-
|
|
31
|
-
# 复制源码与 tsconfig
|
|
32
|
-
COPY tsconfig.json ./
|
|
33
|
-
COPY src/ ./src/
|
|
34
|
-
|
|
35
|
-
# 编译 TypeScript → dist/
|
|
36
|
-
RUN yarn build
|
|
37
|
-
|
|
38
|
-
# ─── 阶段 2:运行 ────────────────────────────────────────────
|
|
39
|
-
FROM node:20-slim AS runner
|
|
40
|
-
|
|
41
|
-
# 时区:与 server 保持一致。debian-slim 不预装 tzdata,需要 apt 装一下。
|
|
42
|
-
RUN apt-get update && \
|
|
43
|
-
apt-get install -y --no-install-recommends tzdata && \
|
|
44
|
-
rm -rf /var/lib/apt/lists/* && \
|
|
45
|
-
ln -snf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \
|
|
46
|
-
echo "Asia/Shanghai" > /etc/timezone
|
|
47
|
-
ENV TZ=Asia/Shanghai
|
|
48
|
-
|
|
49
|
-
WORKDIR /app
|
|
50
|
-
|
|
51
|
-
# 优雅关闭:让 Node 进程(PID 1)直接收到 SIGTERM 后调用 worker.shutdown()
|
|
52
|
-
STOPSIGNAL SIGTERM
|
|
53
|
-
|
|
54
|
-
ENV HUSKY=0 \
|
|
55
|
-
NODE_ENV=production
|
|
56
|
-
|
|
57
|
-
# 先复制依赖描述文件
|
|
58
|
-
COPY --chown=node:node package.json yarn.lock ./
|
|
59
|
-
|
|
60
|
-
# 只安装 production 依赖,大幅减少镜像体积
|
|
61
|
-
RUN --mount=type=cache,target=/root/.yarn \
|
|
62
|
-
yarn install --production --frozen-lockfile
|
|
63
|
-
|
|
64
|
-
# 从构建阶段复制编译产物(不带源码)
|
|
65
|
-
COPY --from=builder --chown=node:node /app/dist ./dist
|
|
66
|
-
|
|
67
|
-
# 切换到非 root 用户(node 镜像内置 uid/gid 1000)
|
|
68
|
-
USER node
|
|
69
|
-
|
|
70
|
-
# Worker 仅出站连接 Temporal Server (7233) 与 PostgreSQL;额外暴露 :8081 供 K8s probe 访问 health 端点
|
|
71
|
-
EXPOSE 8081
|
|
72
|
-
|
|
73
|
-
# 容器健康检查:调用 worker 内置的 /health/live 端点。
|
|
74
|
-
# debian-slim 不带 wget/curl,用 node 自带 http 模块代替(零额外依赖、~10ms 开销)。
|
|
75
|
-
HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \
|
|
76
|
-
CMD node -e "require('http').get('http://127.0.0.1:8081/health/live',r=>process.exit(r.statusCode===200?0:1)).on('error',()=>process.exit(1))" || exit 1
|
|
77
|
-
|
|
78
|
-
# exec form 确保 node 作为 PID 1 直接接收 SIGTERM
|
|
79
|
-
CMD ["node", "dist/index.js"]
|
package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/GRACEFUL-DEPLOYMENT.md
DELETED
|
@@ -1,270 +0,0 @@
|
|
|
1
|
-
# Temporal Worker — Graceful Deployment 方案
|
|
2
|
-
|
|
3
|
-
本文档描述 Temporal Worker 在 K8s 环境下零停机滚动升级的方案,与 `server/GRACEFUL-DEPLOYMENT.md` 形成姊妹文档。Worker 与 server 的区别在于:
|
|
4
|
-
|
|
5
|
-
- **不直接接收用户流量**(不挂 Service / Ingress),所以 Endpoints 摘除一项不适用;
|
|
6
|
-
- **持有的工作单元更长**:单个 Activity / Workflow Task 可能跑数十秒到几分钟;
|
|
7
|
-
- **是 Temporal task queue 的消费者**:被 SIGKILL 后,Temporal Server 会等到 ScheduleToCloseTimeout / 心跳超时才把任务返还队列,造成延迟与重复执行风险。
|
|
8
|
-
|
|
9
|
-
---
|
|
10
|
-
|
|
11
|
-
## 1. 整体时序
|
|
12
|
-
|
|
13
|
-
```
|
|
14
|
-
K8s 发起 Pod 删除(Deployment rolling update)
|
|
15
|
-
│
|
|
16
|
-
└─► 执行 lifecycle.preStop hook
|
|
17
|
-
│
|
|
18
|
-
└─► sleep 5s(让 readinessProbe 失败 + outbox poller 当前批次有缓冲)
|
|
19
|
-
│
|
|
20
|
-
└─► 发送 SIGTERM 给容器主进程(node, PID 1)
|
|
21
|
-
│
|
|
22
|
-
├─► process.on('SIGTERM') handler 触发
|
|
23
|
-
│ ├─► worker.shutdown() // 停止接取新 task
|
|
24
|
-
│ ├─► outboxPoller.stop() // 不再调度下一次 poll
|
|
25
|
-
│ │ ├─► 等待当前 in-flight poll 完成
|
|
26
|
-
│ │ ├─► pg.end()
|
|
27
|
-
│ │ └─► temporalConn.close()
|
|
28
|
-
│ └─► healthServer.close() // 停止响应 K8s probe
|
|
29
|
-
│
|
|
30
|
-
└─► worker.run() 等待所有 in-flight Activity/Workflow Task 完成后 resolve
|
|
31
|
-
└─► connection.close() → 进程退出
|
|
32
|
-
|
|
33
|
-
═══════════════════════════════════════════════════════════════
|
|
34
|
-
terminationGracePeriodSeconds(300s)超时后 → 强制 SIGKILL
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
**关键差异:** Worker 不需要 "等 iptables 同步" 那种延迟,preStop sleep 主要起两个作用:
|
|
38
|
-
1. 让 `/health/ready` 提前 503,K8s probe 能察觉副本即将下线(如有上层调度策略可参考);
|
|
39
|
-
2. 给 outbox poller 当前批次(已 `UPDATE ... SET processed=TRUE` 但还在 `temporalClient.workflow.start` 中的事件)多留几秒缓冲。
|
|
40
|
-
|
|
41
|
-
---
|
|
42
|
-
|
|
43
|
-
## 2. 代码改动
|
|
44
|
-
|
|
45
|
-
### 2.1 `src/index.ts` — 主流程
|
|
46
|
-
|
|
47
|
-
```ts
|
|
48
|
-
process.on('SIGTERM', () => void shutdown('SIGTERM'));
|
|
49
|
-
process.on('SIGINT', () => void shutdown('SIGINT'));
|
|
50
|
-
|
|
51
|
-
await worker.run(); // 阻塞直到 worker.shutdown() 被调用 + tasks drain
|
|
52
|
-
await connection.close(); // worker.run() resolve 之后再关连接
|
|
53
|
-
```
|
|
54
|
-
|
|
55
|
-
`shutdown()` 内部:
|
|
56
|
-
1. `worker.shutdown()` — 同步调用,使 worker 不再接取新 task;
|
|
57
|
-
2. `outboxPoller.stop()` — 取消下一次 setTimeout,等当前 poll 完成,关 PG / Temporal client;
|
|
58
|
-
3. `healthServer.close()` — 停止响应 probe。
|
|
59
|
-
|
|
60
|
-
### 2.2 `src/outbox-poller.ts` — 暴露 stop()
|
|
61
|
-
|
|
62
|
-
`startOutboxPoller()` 现在返回:
|
|
63
|
-
|
|
64
|
-
```ts
|
|
65
|
-
interface OutboxPollerHandle {
|
|
66
|
-
stop(): Promise<void>;
|
|
67
|
-
}
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
`stop()` 行为:
|
|
71
|
-
- 设置 `stopped = true`,下一次循环不再被调度;
|
|
72
|
-
- 若处于 setTimeout 等待中:`clearTimeout` 立即取消;
|
|
73
|
-
- 若处于 in-flight poll 中:等当前 poll 完成(含本轮所有事件处理);
|
|
74
|
-
- 关闭 PG client 与 Temporal Connection。
|
|
75
|
-
|
|
76
|
-
### 2.3 `src/index.ts` — Health 端点
|
|
77
|
-
|
|
78
|
-
| 端点 | 用途 | 行为 |
|
|
79
|
-
|------|------|------|
|
|
80
|
-
| `GET /health/live` | K8s livenessProbe | 进程存活始终 200 |
|
|
81
|
-
| `GET /health/ready` | K8s readinessProbe | `isReady && !isShuttingDown` 才 200,否则 503 |
|
|
82
|
-
|
|
83
|
-
监听端口由 `HEALTH_PORT` 环境变量控制,默认 `8080`。
|
|
84
|
-
|
|
85
|
-
### 2.4 `Dockerfile`
|
|
86
|
-
|
|
87
|
-
```dockerfile
|
|
88
|
-
STOPSIGNAL SIGTERM # 与 K8s 默认信号一致
|
|
89
|
-
EXPOSE 8080 # health 端口
|
|
90
|
-
HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \
|
|
91
|
-
CMD wget -qO- http://127.0.0.1:8080/health/live || exit 1
|
|
92
|
-
CMD ["node", "dist/index.js"] # exec form:node 作为 PID 1 直接收 SIGTERM
|
|
93
|
-
```
|
|
94
|
-
|
|
95
|
-
---
|
|
96
|
-
|
|
97
|
-
## 3. K8s Deployment 配置
|
|
98
|
-
|
|
99
|
-
```yaml
|
|
100
|
-
apiVersion: apps/v1
|
|
101
|
-
kind: Deployment
|
|
102
|
-
metadata:
|
|
103
|
-
name: gadmin-workflow-worker
|
|
104
|
-
spec:
|
|
105
|
-
# ─── 滚动升级策略 ───────────────────────────────────────────
|
|
106
|
-
strategy:
|
|
107
|
-
type: RollingUpdate
|
|
108
|
-
rollingUpdate:
|
|
109
|
-
maxSurge: 1
|
|
110
|
-
maxUnavailable: 1 # Worker 不直接接流量,可以容忍短暂少 1 个副本
|
|
111
|
-
|
|
112
|
-
template:
|
|
113
|
-
spec:
|
|
114
|
-
# ─── 优雅关闭宽限期 ─────────────────────────────────────
|
|
115
|
-
terminationGracePeriodSeconds: 300
|
|
116
|
-
# 给 worker 足够时间完成:
|
|
117
|
-
# - 当前正在执行的 Activity / Workflow Task(最长可能数分钟)
|
|
118
|
-
# - outbox poller 当前批次的事件分发
|
|
119
|
-
# 超过 300s 未退出将被 SIGKILL —— Temporal 端要等心跳超时才能把任务派给其他 worker
|
|
120
|
-
|
|
121
|
-
containers:
|
|
122
|
-
- name: worker
|
|
123
|
-
image: <registry>/<image>:<tag>
|
|
124
|
-
ports:
|
|
125
|
-
- name: health
|
|
126
|
-
containerPort: 8080
|
|
127
|
-
|
|
128
|
-
# ─── preStop Hook ───────────────────────────────────
|
|
129
|
-
lifecycle:
|
|
130
|
-
preStop:
|
|
131
|
-
exec:
|
|
132
|
-
command: ["sh", "-c", "sleep 5"]
|
|
133
|
-
# 让 readinessProbe 提前进入 503 + 给 outbox poller 当前批次缓冲
|
|
134
|
-
|
|
135
|
-
# ─── Readiness Probe ────────────────────────────────
|
|
136
|
-
# Worker 不挂 Service,readinessProbe 主要用来反映"是否健康"给运维面板
|
|
137
|
-
# 也作为部署阶段判断新 Pod 是否启动成功的依据
|
|
138
|
-
readinessProbe:
|
|
139
|
-
httpGet:
|
|
140
|
-
path: /health/ready
|
|
141
|
-
port: 8080
|
|
142
|
-
initialDelaySeconds: 5
|
|
143
|
-
periodSeconds: 5
|
|
144
|
-
failureThreshold: 3
|
|
145
|
-
successThreshold: 1
|
|
146
|
-
|
|
147
|
-
# ─── Liveness Probe ─────────────────────────────────
|
|
148
|
-
# 检测进程僵死(如事件循环被阻塞),失败后 K8s 重启容器
|
|
149
|
-
livenessProbe:
|
|
150
|
-
httpGet:
|
|
151
|
-
path: /health/live
|
|
152
|
-
port: 8080
|
|
153
|
-
initialDelaySeconds: 15
|
|
154
|
-
periodSeconds: 10
|
|
155
|
-
failureThreshold: 3
|
|
156
|
-
|
|
157
|
-
# ─── 资源与环境变量(按需) ─────────────────────────
|
|
158
|
-
env:
|
|
159
|
-
- name: TEMPORAL_ADDRESS
|
|
160
|
-
value: "<temporal-frontend>:7233"
|
|
161
|
-
- name: TEMPORAL_NAMESPACE
|
|
162
|
-
value: "default"
|
|
163
|
-
- name: TEMPORAL_TASK_QUEUE
|
|
164
|
-
value: "workflow-execution"
|
|
165
|
-
- name: DATABASE_URL
|
|
166
|
-
valueFrom:
|
|
167
|
-
secretKeyRef:
|
|
168
|
-
name: gadmin-db
|
|
169
|
-
key: url
|
|
170
|
-
```
|
|
171
|
-
|
|
172
|
-
---
|
|
173
|
-
|
|
174
|
-
## 4. 配置说明
|
|
175
|
-
|
|
176
|
-
### 4.1 为什么 terminationGracePeriodSeconds = 300?
|
|
177
|
-
|
|
178
|
-
| 因素 | 说明 |
|
|
179
|
-
|------|------|
|
|
180
|
-
| preStop sleep | 5s |
|
|
181
|
-
| 当前 in-flight Activity / Workflow Task | 最长按业务实际定义,常见 30s–几分钟 |
|
|
182
|
-
| 当前 in-flight outbox poll 批次 | 通常 < 5s(仅启动 Temporal workflow,不等执行) |
|
|
183
|
-
| **总计余量** | 300s 覆盖大多数场景 |
|
|
184
|
-
|
|
185
|
-
如果你的 Activity 可能执行超过 295s(300 − 5s preStop),有两条路:
|
|
186
|
-
1. **加大 `terminationGracePeriodSeconds`**(例如长跑批量任务);
|
|
187
|
-
2. **让 Activity 支持心跳取消**:`Context.current().heartbeat()` + `Context.current().cancellationSignal`,shutdown 时 worker 会向 Activity 发送 cancellation,business code 可以选择中断并提前返回,让 Temporal 把任务重派给其他 worker。
|
|
188
|
-
|
|
189
|
-
### 4.2 maxUnavailable 与 server 的差异
|
|
190
|
-
|
|
191
|
-
server 用 `maxUnavailable: 0`,因为它直面用户流量、Pod 数变少会立刻反映为请求失败。Worker 不接流量,副本短暂少一个不影响 Temporal 的 task 派发(Temporal 自己有 polling 模型 + 流控),所以这里放宽到 `1` 减少升级耗时。如对延迟敏感(例如就只跑 1 副本),可以收紧到 `0`。
|
|
192
|
-
|
|
193
|
-
### 4.3 Outbox poller 的语义边界
|
|
194
|
-
|
|
195
|
-
⚠️ **当前实现有一个独立于 graceful 的语义问题**:`UPDATE ... SET processed = TRUE` 与 `temporalClient.workflow.start(...)` 不在同一事务里。如果在两者之间被 SIGKILL(或 stop() 等到 grace 超时),事件已被标记为 processed 但 workflow 实例没建立,下一次 poll 不会再处理它。
|
|
196
|
-
|
|
197
|
-
graceful 关闭把这个窗口最小化(一定会等当前 poll 内的 `for (const row of rows)` 循环走完),但**没有彻底消除**。彻底解决需要把 `UPDATE` 与 `INSERT t_workflow_instance + workflow.start` 改造为:先 INSERT instance + 启 workflow,成功后再 mark outbox processed —— 这是另一个独立的修复。
|
|
198
|
-
|
|
199
|
-
---
|
|
200
|
-
|
|
201
|
-
## 5. 验证方法
|
|
202
|
-
|
|
203
|
-
### 5.1 本地验证 Shutdown
|
|
204
|
-
|
|
205
|
-
```bash
|
|
206
|
-
cd temporal/worker && yarn build && yarn start &
|
|
207
|
-
WORKER_PID=$!
|
|
208
|
-
|
|
209
|
-
# 等服务启动
|
|
210
|
-
sleep 3
|
|
211
|
-
|
|
212
|
-
# 验证 health
|
|
213
|
-
curl -s http://localhost:8080/health/live # {"status":"ok"}
|
|
214
|
-
curl -s http://localhost:8080/health/ready # {"status":"ok"}
|
|
215
|
-
|
|
216
|
-
# 发送 SIGTERM
|
|
217
|
-
kill -TERM $WORKER_PID
|
|
218
|
-
|
|
219
|
-
# 期望日志依次出现:
|
|
220
|
-
# [Worker] SIGTERM received, beginning graceful shutdown...
|
|
221
|
-
# [Worker] worker.shutdown() called, draining in-flight tasks
|
|
222
|
-
# [OutboxPoller] Stopped, resources released
|
|
223
|
-
# [Worker] Health server closed
|
|
224
|
-
# [Worker] Temporal connection closed. Bye.
|
|
225
|
-
# 进程以 exit code 0 退出
|
|
226
|
-
```
|
|
227
|
-
|
|
228
|
-
### 5.2 K8s 环境验证
|
|
229
|
-
|
|
230
|
-
```bash
|
|
231
|
-
# 观察滚动升级
|
|
232
|
-
kubectl rollout status deployment/gadmin-workflow-worker -w
|
|
233
|
-
|
|
234
|
-
# 看 Pod 事件确认 preStop 执行
|
|
235
|
-
kubectl describe pod <pod-name> | grep -A5 "Events"
|
|
236
|
-
|
|
237
|
-
# 升级期间持续触发事件,验证不丢任务
|
|
238
|
-
# (根据业务,向能触发 outbox 的表持续写入,再去 t_workflow_instance 比对)
|
|
239
|
-
```
|
|
240
|
-
|
|
241
|
-
---
|
|
242
|
-
|
|
243
|
-
## 6. 信号传递链路
|
|
244
|
-
|
|
245
|
-
```
|
|
246
|
-
K8s kubelet
|
|
247
|
-
│
|
|
248
|
-
└─► containerd: 发送 STOPSIGNAL (SIGTERM)
|
|
249
|
-
│
|
|
250
|
-
└─► Node.js process(PID 1,因 CMD 是 exec form)
|
|
251
|
-
│
|
|
252
|
-
├─► process.on('SIGTERM') handler
|
|
253
|
-
│ ├─► worker.shutdown() → worker.run() 进入 draining 状态
|
|
254
|
-
│ ├─► outboxPoller.stop() → 关 PG / Temporal client
|
|
255
|
-
│ └─► healthServer.close()
|
|
256
|
-
│
|
|
257
|
-
└─► await worker.run() resolve → await connection.close() → 进程退出
|
|
258
|
-
```
|
|
259
|
-
|
|
260
|
-
---
|
|
261
|
-
|
|
262
|
-
## 7. 故障场景与应对
|
|
263
|
-
|
|
264
|
-
| 场景 | 现象 | 应对 |
|
|
265
|
-
|------|------|------|
|
|
266
|
-
| Activity 执行时间超过 grace period | 被 SIGKILL;Temporal 等心跳超时后重派 | 让 Activity 支持心跳 + cancellation;或加大 grace period |
|
|
267
|
-
| outbox poll 中途被 SIGKILL | 已 mark processed 但未启 workflow 的事件丢失 | 见 §4.3,需独立修复事务边界 |
|
|
268
|
-
| readinessProbe 失败 | 仅影响监控/调度面板;不影响 task 派发 | 检查日志、PG / Temporal 连接 |
|
|
269
|
-
| livenessProbe 失败 | K8s 重启容器(同样走 SIGTERM → graceful path) | 排查事件循环阻塞(同步重 IO?deadlock?) |
|
|
270
|
-
| `worker.shutdown()` 后迟迟不 resolve | 一定有 Activity 长时间不返回 | 最终 SIGKILL,Temporal 端等超时;治本看 §4.1 第 2 项 |
|