@gadmin2n/schematics 0.0.136 → 0.0.137

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.
Files changed (94) hide show
  1. package/dist/lib/application/files/gadmin2-game-angle-demo/Dockerfile.server +2 -2
  2. package/dist/lib/application/files/gadmin2-game-angle-demo/compose-ctl.sh +1 -1
  3. package/dist/lib/application/files/gadmin2-game-angle-demo/config/prisma/canvas.prisma +1 -0
  4. package/dist/lib/application/files/gadmin2-game-angle-demo/config/prisma/workflow.prisma +14 -3
  5. package/dist/lib/application/files/gadmin2-game-angle-demo/config/ui/User.ts +4 -4
  6. package/dist/lib/application/files/gadmin2-game-angle-demo/docker-compose.md +2 -2
  7. package/dist/lib/application/files/gadmin2-game-angle-demo/docker-compose.yml +13 -92
  8. package/dist/lib/application/files/gadmin2-game-angle-demo/readme.md +105 -7
  9. package/dist/lib/application/files/gadmin2-game-angle-demo/server/ensure-database.js +1 -1
  10. package/dist/lib/application/files/gadmin2-game-angle-demo/server/package.json +5 -5
  11. package/dist/lib/application/files/gadmin2-game-angle-demo/server/prisma.config.ts +9 -1
  12. package/dist/lib/application/files/gadmin2-game-angle-demo/server/scripts/notify-dev-servers.js +9 -8
  13. package/dist/lib/application/files/gadmin2-game-angle-demo/server/scripts/wait-for-nest-ready.js +5 -5
  14. package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/workflow-node-types.ts +43 -2
  15. package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/workflow.seed.ts +9 -1
  16. package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/workflows.ts +24 -0
  17. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/canvas/canvas-export.dto.ts +27 -0
  18. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/canvas/canvas-export.service.spec.ts +430 -0
  19. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/canvas/canvas-export.service.ts +203 -0
  20. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/canvas/canvas.controller.ts +36 -3
  21. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/canvas/canvas.module.ts +2 -1
  22. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/temporalTenantConfig/temporalTenantConfig.controller.spec.ts +54 -0
  23. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/temporalTenantConfig/temporalTenantConfig.controller.ts +245 -0
  24. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/temporalTenantConfig/temporalTenantConfig.module.ts +10 -0
  25. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/temporalTenantConfig/temporalTenantConfig.service.spec.ts +366 -0
  26. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/temporalTenantConfig/temporalTenantConfig.service.ts +110 -0
  27. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-execution.dto.ts +19 -1
  28. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.controller.ts +15 -0
  29. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.service.spec.ts +79 -0
  30. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.service.ts +41 -19
  31. package/dist/lib/application/files/gadmin2-game-angle-demo/server/start-prod.sh +5 -5
  32. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/README.md +37 -573
  33. package/dist/lib/application/files/gadmin2-game-angle-demo/web/ecosystem.dev.config.cjs +12 -1
  34. package/dist/lib/application/files/gadmin2-game-angle-demo/web/package.json +1 -0
  35. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/layout/header.tsx +3 -2
  36. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/config/routeRegistry.tsx +39 -26
  37. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/i18n.ts +3 -1
  38. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/locales/en/common.json +3 -0
  39. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/locales/zh_CN/common.json +3 -0
  40. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/CanvasEditPage.tsx +23 -5
  41. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/CanvasListPage.tsx +66 -10
  42. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/CanvasPage.tsx +16 -7
  43. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/CanvasToolbar.tsx +21 -0
  44. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/canvasApi.ts +30 -0
  45. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/components/ExportModal.tsx +84 -0
  46. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/components/ImportModal.tsx +166 -0
  47. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/hooks/useCanvasHistory.ts +52 -13
  48. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/DslView.tsx +6 -0
  49. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/NodePropertyPanel.tsx +6 -1
  50. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/editor.tsx +17 -0
  51. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/instance-detail.tsx +18 -6
  52. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/temporal-tenant/index.tsx +225 -0
  53. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/utilities/lazy-with-reload.ts +31 -0
  54. package/package.json +1 -1
  55. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-dsl-validate.spec.ts +0 -220
  56. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-dsl-validate.ts +0 -132
  57. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/TODO.md +0 -152
  58. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/config/development-sql.yaml +0 -5
  59. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/docker-compose.yml +0 -25
  60. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/package.json +0 -13
  61. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/.dockerignore +0 -12
  62. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/.env +0 -7
  63. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/Dockerfile +0 -79
  64. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/GRACEFUL-DEPLOYMENT.md +0 -270
  65. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/SANDBOX.md +0 -122
  66. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/package-lock.json +0 -4285
  67. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/package.json +0 -28
  68. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/activities/code-execute.ts +0 -51
  69. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/activities/db-execute.ts +0 -85
  70. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/activities/db-query.ts +0 -35
  71. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/activities/http-request.ts +0 -54
  72. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/activities/index.ts +0 -6
  73. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/activities/reporting.ts +0 -85
  74. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/activities/send-notification.ts +0 -47
  75. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/config.ts +0 -13
  76. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/dsl/condition.ts +0 -101
  77. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/dsl/context.ts +0 -58
  78. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/dsl/graph.ts +0 -184
  79. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/dsl/helpers.ts +0 -133
  80. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/dsl/node-types.ts +0 -96
  81. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/dsl/types.ts +0 -77
  82. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/dsl/validate.ts +0 -109
  83. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/index.ts +0 -101
  84. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/outbox-poller.ts +0 -382
  85. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/tests/activities/code-execute.test.ts +0 -44
  86. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/tests/activities/http-request.test.ts +0 -87
  87. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/tests/cron-trigger-workflow.test.ts +0 -20
  88. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/tests/helpers.test.ts +0 -225
  89. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/tests/node-type-consistency.test.ts +0 -101
  90. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/tests/validate.test.ts +0 -205
  91. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/workflows/dsl-workflow.ts +0 -499
  92. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/tsconfig.json +0 -19
  93. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/vitest.config.ts +0 -8
  94. 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,5 +0,0 @@
1
- # Temporal dynamic configuration for development
2
- # See: https://docs.temporal.io/references/dynamic-configuration
3
- system.forceSearchAttributesCacheRefreshOnRead:
4
- - value: true
5
- constraints: {}
@@ -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,12 +0,0 @@
1
- node_modules
2
- dist
3
- .env.*
4
- .git
5
- *.log
6
- .claude
7
- .vscode
8
- .idea
9
- tests
10
- vitest.config.ts
11
- *.md
12
- coverage
@@ -1,7 +0,0 @@
1
- # Temporal Worker Environment
2
- TEMPORAL_ADDRESS=localhost:7233
3
- TEMPORAL_NAMESPACE=default
4
- TEMPORAL_TASK_QUEUE=workflow-execution
5
-
6
- # Database (same as server)
7
- DATABASE_URL=postgresql://kavenma:kavenma@127.0.0.1:5432/gadmin_demo?schema=public
@@ -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"]
@@ -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 项 |