@gadmin2n/schematics 0.0.88 → 0.0.90
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/.dockerignore +16 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/Dockerfile.codegen +40 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/Dockerfile.server +76 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/Dockerfile.web +53 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/Jenkinsfile +219 -33
- package/dist/lib/application/files/gadmin2-game-angle-demo/compose-ctl.sh +250 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/config/prisma/workflow.prisma +4 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/dev/postgres/init.sql +12 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/docker-compose.md +170 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/docker-compose.yml +254 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/package.json +7 -6
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/scripts/lib/page-helpers.ts +1 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/scripts/prismaModels.ts +1 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/agenda.seed.ts +39 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/audit.seed.ts +40 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/bootstrap.ts +56 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/canvas.seed.ts +39 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/{scripts/sync-data-mngt-pages.ts → seed/data-mngt.seed.ts} +36 -20
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/game.seed.ts +44 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/index.ts +30 -6
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/permission.seed.ts +130 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/workflow-event-trigger.ts +60 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/workflow-node-types.ts +11 -25
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/workflow.seed.ts +108 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/main.ts +1 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/agendaJob/agendaJob.controller.spec.ts +31 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/audit/audit.controller.spec.ts +31 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/audit/audit.service.spec.ts +41 -57
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/game/game.controller.spec.ts +31 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/game/game.service.spec.ts +309 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/page/page.controller.spec.ts +31 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/page/page.service.spec.ts +315 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/pageResource/pageResource.controller.spec.ts +31 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/pageResource/pageResource.service.spec.ts +312 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/resource/resource.controller.spec.ts +31 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/resource/resource.service.spec.ts +317 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/role/role.controller.spec.ts +31 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/role/role.service.spec.ts +309 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/rolePages/rolePages.controller.spec.ts +31 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/rolePages/rolePages.service.spec.ts +299 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/roleResource/roleResource.controller.spec.ts +31 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/roleResource/roleResource.service.spec.ts +307 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/user/user.controller.spec.ts +31 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/user/user.service.spec.ts +309 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/dsl-validate.util.spec.ts +205 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/dsl-validate.util.ts +116 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/temporal.service.spec.ts +158 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/temporal.service.ts +110 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/webhook-signature.util.spec.ts +79 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/webhook-signature.util.ts +54 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.controller.ts +34 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.service.spec.ts +457 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.service.ts +241 -4
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflowEventOutbox/workflowEventOutbox.controller.spec.ts +34 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflowEventOutbox/workflowEventOutbox.service.spec.ts +24 -30
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflowNodeInstance/workflowNodeInstance.controller.spec.ts +34 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflowNodeInstance/workflowNodeInstance.service.spec.ts +36 -36
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflowNodeType/workflowNodeType.controller.spec.ts +34 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflowNodeType/workflowNodeType.service.spec.ts +48 -24
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/README.md +312 -3
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/TODO.md +152 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/.dockerignore +12 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/Dockerfile +79 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/GRACEFUL-DEPLOYMENT.md +270 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/activities/index.ts +1 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/activities/reporting.ts +23 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/index.ts +70 -5
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/outbox-poller.ts +246 -90
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/tests/cron-trigger-workflow.test.ts +20 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/workflows/dsl-workflow.ts +96 -8
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/nginx.conf +74 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/agentPanel/ElementInspector.tsx +18 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/agentPanel/promptGenerator.ts +1 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/helpers/form.tsx +1 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/locales/en/common.json +3 -3
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/locales/zh_CN/common.json +3 -3
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/plugins/devShellPlugin.ts +4 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/CanvasEditPage.tsx +9 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/CanvasListPage.tsx +156 -139
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/CanvasPage.tsx +14 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/CanvasToolbar.tsx +62 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/PublishModal.tsx +4 -6
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/canvasApi.ts +18 -27
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/canvasDefaults.ts +32 -11
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/demos.ts +48 -61
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas-page/index.tsx +3 -6
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/DslView.tsx +16 -16
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/editor.tsx +28 -35
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/index.tsx +1 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/instance-detail.tsx +34 -3
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/show.tsx +1 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/types.ts +1 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/styles/antd.css +6 -0
- package/package.json +1 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/.gitattributes +0 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/Dockerfile +0 -63
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/scripts/sync-resources.ts +0 -100
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/permissions.ts +0 -302
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/canvas/canvas.controller.spec.ts +0 -20
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/sql/create-event-trigger.sql +0 -87
- /package/dist/lib/application/files/gadmin2-game-angle-demo/{GRACEFUL-DEPLOYMENT.md → server/GRACEFUL-DEPLOYMENT.md} +0 -0
|
@@ -48,8 +48,6 @@ temporal/
|
|
|
48
48
|
├── docker-compose.yml # Temporal Server + Web UI
|
|
49
49
|
├── config/
|
|
50
50
|
│ └── development-sql.yaml # Server 动态配置
|
|
51
|
-
├── sql/
|
|
52
|
-
│ └── create-event-trigger.sql # Outbox 表 + PG 触发器函数
|
|
53
51
|
└── worker/ # DSL 解释器 Worker
|
|
54
52
|
├── package.json
|
|
55
53
|
├── tsconfig.json
|
|
@@ -120,6 +118,311 @@ npm run worker:dev
|
|
|
120
118
|
7. Workflow 完成/失败 → 更新 t_workflow_instance 状态
|
|
121
119
|
```
|
|
122
120
|
|
|
121
|
+
## 数据库表分工
|
|
122
|
+
|
|
123
|
+
Temporal 体系下有 **两类 PG 表**,分布在不同 schema,互不干扰:
|
|
124
|
+
|
|
125
|
+
| 类别 | 所在 schema | 谁来写 | 典型表 | 用途 |
|
|
126
|
+
|------|------------|--------|--------|------|
|
|
127
|
+
| **Temporal 系统表** | `temporal` (Server 启动时自动建表) | 仅 Temporal Server | `executions`、`history_node`、`history_tree`、`current_executions`、`task_queues`、`tasks`、`timer_tasks`、`shard`、`namespaces` | Workflow 状态机、事件历史、任务队列、定时器、分片协调 |
|
|
128
|
+
| **业务表** | `public` (Prisma 管理) | NestJS / Worker Activity | `t_workflow`、`t_workflow_version`、`t_workflow_instance`、`t_workflow_node_execution`、`t_workflow_event_outbox` | 业务可见的工作流定义、实例、节点执行记录、事件触发 outbox |
|
|
129
|
+
|
|
130
|
+
> Temporal Server **从不读写**业务表;业务代码(NestJS / Worker Activity)**从不读写** Temporal 系统表。两者通过 `temporal_run_id` 字段(保存在 `t_workflow_instance` 上)建立关联。
|
|
131
|
+
|
|
132
|
+
## 两种触发场景的时序图
|
|
133
|
+
|
|
134
|
+
### 场景 A:NestJS 主动调用 `temporalService.startWorkflow()`
|
|
135
|
+
|
|
136
|
+
入口:用户在前端点 "Run",或 Agenda 定时任务 / 其他业务代码主动调用 `WorkflowService.executeWorkflow()`。
|
|
137
|
+
|
|
138
|
+
```
|
|
139
|
+
┌────────┐ ┌──────────────────┐ ┌─────────────────┐ ┌────────────────┐ ┌────────────┐
|
|
140
|
+
│Frontend│ │ NestJS │ │ PostgreSQL │ │ Temporal Server│ │ Worker │
|
|
141
|
+
│ │ │ WorkflowService │ │ (业务 schema) │ │ (gRPC :7233) │ │ (long-poll)│
|
|
142
|
+
└───┬────┘ └────────┬─────────┘ └────────┬────────┘ └───────┬────────┘ └─────┬──────┘
|
|
143
|
+
│ POST /run │ │ │ │
|
|
144
|
+
├────────────────►│ │ │ │
|
|
145
|
+
│ │ SELECT t_workflow + t_workflow_version │ │
|
|
146
|
+
│ ├─────────────────────►│ │ │
|
|
147
|
+
│ │◄─────────────────────┤ 校验 PUBLISHED/enabled │
|
|
148
|
+
│ │ │ │ │
|
|
149
|
+
│ │ INSERT t_workflow_instance (status=PENDING)│ │
|
|
150
|
+
│ ├─────────────────────►│ │ │
|
|
151
|
+
│ │◄──── instanceId ─────┤ │ │
|
|
152
|
+
│ │ │ │ │
|
|
153
|
+
│ │ temporalService.startWorkflow('dslWorkflow', …) │
|
|
154
|
+
│ ├──────────────────────────────────────────►│ │
|
|
155
|
+
│ │ │ 写 temporal.executions / history_node │
|
|
156
|
+
│ │ │ + 投递 WorkflowTask 到 task_queues │
|
|
157
|
+
│ │ │◄───────────────────┤ │
|
|
158
|
+
│ │◄──── temporalRunId ──────────────────────┤ │
|
|
159
|
+
│ │ │ │ │
|
|
160
|
+
│ │ UPDATE t_workflow_instance SET temporal_run_id│ │
|
|
161
|
+
│ ├─────────────────────►│ │ │
|
|
162
|
+
│ │ │ │ PollWorkflowTask │
|
|
163
|
+
│ │ │ │◄──────────────────┤
|
|
164
|
+
│ │ │ ├──── task ────────►│
|
|
165
|
+
│ │ │ │ │ 执行 dslWorkflow()
|
|
166
|
+
│ │ │ │ │ ──┐
|
|
167
|
+
│ │ │ │ ScheduleActivity │ <─┘
|
|
168
|
+
│ │ │ │◄──────────────────┤
|
|
169
|
+
│ │ │ ├── activity task ─►│
|
|
170
|
+
│ │ │ │ │ Activity 内调用
|
|
171
|
+
│ │ │ │ │ reporting.ts:
|
|
172
|
+
│ │ │◄────── INSERT/UPDATE t_workflow_node_execution ─┤
|
|
173
|
+
│ │ │◄────── UPDATE t_workflow_instance (RUNNING/DONE)┤
|
|
174
|
+
│ │ │ │ CompleteActivity │
|
|
175
|
+
│ │ │ │◄──────────────────┤
|
|
176
|
+
│ │ │ │ │
|
|
177
|
+
│ │ │ │ … 直到 Workflow 完成 …
|
|
178
|
+
│◄────── 200 OK { instanceId, temporalRunId } ────────────────┤ │
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
**涉及的 PG 表:**
|
|
182
|
+
|
|
183
|
+
| 表 | 操作 | 写入方 |
|
|
184
|
+
|----|------|--------|
|
|
185
|
+
| `t_workflow`、`t_workflow_version` | SELECT | NestJS(取 DSL + 校验状态) |
|
|
186
|
+
| `t_workflow_instance` | INSERT (PENDING) → UPDATE (temporal_run_id) → UPDATE (status) | NestJS 创建;Worker `reporting.ts` 在执行中改 |
|
|
187
|
+
| `t_workflow_node_execution` | INSERT/UPDATE 每个节点 | Worker `reporting.ts`(Activity 内调用) |
|
|
188
|
+
| `temporal.executions` / `current_executions` | INSERT (新 WorkflowExecution) → UPDATE 状态 | Temporal Server(Frontend / History Service) |
|
|
189
|
+
| `temporal.history_node` / `history_tree` | INSERT 事件(Started / ActivityScheduled / Completed …) | Temporal Server |
|
|
190
|
+
| `temporal.task_queues` / `tasks` | INSERT/DELETE Workflow & Activity Task | Temporal Server(Matching Service) |
|
|
191
|
+
| `temporal.timer_tasks` | 节点超时、Workflow 总超时、Sleep 节点 | Temporal Server |
|
|
192
|
+
|
|
193
|
+
---
|
|
194
|
+
|
|
195
|
+
### 场景 B:Worker `outbox-poller` 因业务表变更触发
|
|
196
|
+
|
|
197
|
+
入口:业务表(如 `t_order`)发生 INSERT/UPDATE/DELETE → AFTER 触发器写一行到 `t_workflow_event_outbox` → Worker 进程内 outbox-poller 扫到。
|
|
198
|
+
|
|
199
|
+
```
|
|
200
|
+
┌────────────┐ ┌────────────────┐ ┌─────────────────────┐ ┌────────────────┐ ┌──────────────┐
|
|
201
|
+
│ 业务来源 │ │ PostgreSQL │ │ Worker │ │ Temporal Server│ │ Worker │
|
|
202
|
+
│ (任意 INSERT│ │ (业务 schema) │ │ outbox-poller.ts │ │ (gRPC :7233) │ │ dslWorkflow │
|
|
203
|
+
│ /UPDATE) │ │ │ │ (Worker 进程内) │ │ │ │ (同一进程) │
|
|
204
|
+
└─────┬──────┘ └───────┬────────┘ └──────────┬──────────┘ └───────┬────────┘ └──────┬───────┘
|
|
205
|
+
│ INSERT/UPDATE t_order │ │ │
|
|
206
|
+
├─────────────────►│ │ │ │
|
|
207
|
+
│ │ AFTER TRIGGER notify_workflow_event() │ │
|
|
208
|
+
│ │ INSERT t_workflow_event_outbox (同事务) │ │
|
|
209
|
+
│ │ │ │ │
|
|
210
|
+
│ │ ◄── 每 10s ───────── │ UPDATE outbox │ │
|
|
211
|
+
│ │ │ SET processed=true │ │
|
|
212
|
+
│ │ │ ... FOR UPDATE SKIP LOCKED │
|
|
213
|
+
│ ├──── 未处理事件批 ────►│ │ │
|
|
214
|
+
│ │ │ │ │
|
|
215
|
+
│ │ SELECT t_workflow JOIN t_workflow_version │ │
|
|
216
|
+
│ │ WHERE status='PUBLISHED' AND is_enabled │ │
|
|
217
|
+
│ ├──────────────────────►│ matchEventName + filterExpression │
|
|
218
|
+
│ │ │ │ │
|
|
219
|
+
│ │ INSERT t_workflow_instance (creator='event_trigger') │
|
|
220
|
+
│ │◄──────────────────────┤ │ │
|
|
221
|
+
│ ├──── instanceId ──────►│ │ │
|
|
222
|
+
│ │ │ temporalClient.workflow.start('dslWorkflow', …)
|
|
223
|
+
│ │ ├─────────────────────►│ │
|
|
224
|
+
│ │ │ │ 写 temporal.executions
|
|
225
|
+
│ │ │ │ + 投递 WorkflowTask│
|
|
226
|
+
│ │ │◄────── runId ────────┤ │
|
|
227
|
+
│ │ UPDATE t_workflow_instance SET temporal_run_id│ │
|
|
228
|
+
│ │◄──────────────────────┤ │ │
|
|
229
|
+
│ │ │ │ PollWorkflowTask │
|
|
230
|
+
│ │ │ │◄──────────────────┤
|
|
231
|
+
│ │ │ ├──── task ────────►│
|
|
232
|
+
│ │ │ │ │ 执行节点 + Activity
|
|
233
|
+
│ │◄──────── INSERT/UPDATE t_workflow_node_execution ─────────────────┤
|
|
234
|
+
│ │◄──────── UPDATE t_workflow_instance (status, ended_at) ──────────┤
|
|
235
|
+
│ │ │ │ │
|
|
236
|
+
│ │ (失败时) UPDATE t_workflow_event_outbox │ │
|
|
237
|
+
│ │ SET processed=false, retry_count=n │ │
|
|
238
|
+
│ │◄──────────────────────┤ │ │
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
**涉及的 PG 表:**
|
|
242
|
+
|
|
243
|
+
| 表 | 操作 | 写入方 |
|
|
244
|
+
|----|------|--------|
|
|
245
|
+
| 业务表(如 `t_order`) | 任意 DML | 业务来源 |
|
|
246
|
+
| `t_workflow_event_outbox` | INSERT(触发器,业务事务内)→ UPDATE processed/retry_count(poller) | DB Trigger + outbox-poller |
|
|
247
|
+
| `t_workflow`、`t_workflow_version` | SELECT 已发布且启用的最新版本 | outbox-poller |
|
|
248
|
+
| `t_workflow_instance` | INSERT (PENDING, creator='event_trigger') → UPDATE temporal_run_id → UPDATE status | outbox-poller 创建;Worker `reporting.ts` 在执行中改 |
|
|
249
|
+
| `t_workflow_node_execution` | INSERT/UPDATE 每个节点 | Worker `reporting.ts` |
|
|
250
|
+
| `temporal.executions` / `history_node` / `task_queues` / `tasks` / `timer_tasks` | 同场景 A | Temporal Server |
|
|
251
|
+
|
|
252
|
+
---
|
|
253
|
+
|
|
254
|
+
---
|
|
255
|
+
|
|
256
|
+
### 场景 C:Temporal Schedule API(定时任务触发,`cron_trigger`)
|
|
257
|
+
|
|
258
|
+
> ✅ **当前状态**:已实现。`WorkflowService.publish()` 解析 DSL 中的 `cron_trigger` 节点,调 `TemporalService.upsertCronSchedule()` 注册 Temporal Schedule;`toggleEnabled` / `remove` 联动 pause/unpause/delete schedule。Worker 新增 `cronTriggerWorkflow`,由 Schedule 触发后创建 `t_workflow_instance(creator='cron')` 并以子工作流方式执行 `dslWorkflow`。
|
|
259
|
+
|
|
260
|
+
入口:Workflow 中包含 `cron_trigger` 节点(带 `cronExpression` 配置),点击 Publish 时由 NestJS 把 cron 表达式注册到 Temporal Server 的 Schedule 子系统;之后由 **Server 自己**按时间触发,无需 NestJS / Worker 长跑组件。
|
|
261
|
+
|
|
262
|
+
```
|
|
263
|
+
┌────────┐ ┌──────────────────┐ ┌─────────────────┐ ┌──────────────────────────┐ ┌────────────┐
|
|
264
|
+
│Frontend│ │ NestJS │ │ PostgreSQL │ │ Temporal Server │ │ Worker │
|
|
265
|
+
│ │ │ WorkflowService │ │ (业务 schema) │ │ (Schedule + Workflow) │ │ (long-poll)│
|
|
266
|
+
└───┬────┘ └────────┬─────────┘ └────────┬────────┘ └───────┬──────────────────┘ └─────┬──────┘
|
|
267
|
+
│ POST /:id/publish│ │ │ │
|
|
268
|
+
├────────────────►│ │ │ │
|
|
269
|
+
│ │ 解析 DSL:找 cron_trigger 节点 │ │
|
|
270
|
+
│ │ UPDATE t_workflow SET status='PUBLISHED' │ │
|
|
271
|
+
│ ├─────────────────────►│ │ │
|
|
272
|
+
│ │ │ │ │
|
|
273
|
+
│ │ client.schedule.create({ │ │
|
|
274
|
+
│ │ scheduleId: `wf-${workflowId}-cron`, │ │
|
|
275
|
+
│ │ spec:{ cronExpressions:['0 2 * * *'] }, │ │
|
|
276
|
+
│ │ action:{ type:'startWorkflow', │ │
|
|
277
|
+
│ │ workflowType:'cronTriggerWorkflow', │ │
|
|
278
|
+
│ │ args:[{ workflowId, versionId, dsl }] │ │
|
|
279
|
+
│ │ } │ │
|
|
280
|
+
│ │ }) │ │
|
|
281
|
+
│ ├──────────────────────────────────────────►│ │
|
|
282
|
+
│ │ │ │ Server 写 temporal.schedules + │
|
|
283
|
+
│ │ │ │ 内部 timer 任务 │
|
|
284
|
+
│ │◄──── ok ──────────────────────────────────┤ │
|
|
285
|
+
│◄── 200 OK ──────┤ │ │ │
|
|
286
|
+
│ │ │ │ │
|
|
287
|
+
⋮ ⋮ ⋮ (等到 cron 时间到,Server 自动起 Workflow) ⋮
|
|
288
|
+
│ │ │ │ │
|
|
289
|
+
│ │ │ │ Server: 写 executions / │
|
|
290
|
+
│ │ │ │ history_node + 投 WorkflowTask│
|
|
291
|
+
│ │ │ │ │
|
|
292
|
+
│ │ │ │ PollWorkflowTask│
|
|
293
|
+
│ │ │ │◄────────────────────────────┤
|
|
294
|
+
│ │ │ ├──── task ──────────────────►│
|
|
295
|
+
│ │ │ │ │ 1) cronTriggerWorkflow:
|
|
296
|
+
│ │ │ │ │ Activity:createInstance
|
|
297
|
+
│ │ │ INSERT t_workflow_instance (creator='cron') │
|
|
298
|
+
│ │ │◄──────────────────────────────────────────────── ┤
|
|
299
|
+
│ │ │ │ │ 2) executeChildWorkflow
|
|
300
|
+
│ │ │ │ │ ('dslWorkflow', { instanceId, dsl, … })
|
|
301
|
+
│ │ │ │ │ ─→ 与场景 A 第 5 步起完全一致
|
|
302
|
+
│ │ │ INSERT/UPDATE t_workflow_node_execution │
|
|
303
|
+
│ │ │◄──────────────────────────────────────────────── ┤
|
|
304
|
+
│ │ │ UPDATE t_workflow_instance (status, ended_at) │
|
|
305
|
+
│ │ │◄──────────────────────────────────────────────── ┤
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
**编辑/取消调度时:**
|
|
309
|
+
|
|
310
|
+
| 用户操作 | NestJS 调用 | 说明 |
|
|
311
|
+
|----------|-------------|------|
|
|
312
|
+
| 修改 cron 表达式(发布新版本) | `client.schedule.getHandle(id).update(...)` | 原 schedule 改 spec,不会丢历史 |
|
|
313
|
+
| 暂停 / 启用 Workflow | `handle.pause()` / `handle.unpause()` | Server 跳过触发但保留 schedule |
|
|
314
|
+
| 删除 Workflow / 取消发布 | `handle.delete()` | 同时清掉 schedule,避免遗留 |
|
|
315
|
+
|
|
316
|
+
**涉及的 PG 表:**
|
|
317
|
+
|
|
318
|
+
| 表 | 操作 | 写入方 |
|
|
319
|
+
|----|------|--------|
|
|
320
|
+
| `t_workflow` | UPDATE status | NestJS(发布动作) |
|
|
321
|
+
| `t_workflow_instance` | INSERT (creator='cron') → UPDATE status | Worker `cronTriggerWorkflow` 内的 createInstance Activity |
|
|
322
|
+
| `t_workflow_node_execution` | INSERT/UPDATE | Worker `reporting.ts` |
|
|
323
|
+
| `temporal.schedules` / `schedules_by_namespace` | INSERT/UPDATE/DELETE schedule 记录 | Temporal Server(Frontend Service) |
|
|
324
|
+
| `temporal.executions` / `history_node` / `task_queues` | 同场景 A | Temporal Server(Schedule 触发后) |
|
|
325
|
+
|
|
326
|
+
**为什么不用 outbox-poller 模式 / AgendaService?**
|
|
327
|
+
|
|
328
|
+
| 方案 | 缺点 |
|
|
329
|
+
|------|------|
|
|
330
|
+
| Worker 内 cron-poller(仿 outbox-poller) | 多 Worker 实例需自己加分布式锁,且会跟 Temporal 状态机割裂 |
|
|
331
|
+
| 复用 AgendaService | Agenda 跑在 NestJS 进程,Workflow 调度跨两个时序源,故障定位复杂 |
|
|
332
|
+
| **Temporal Schedule API** ✅ | Server 原生支持,自动持久化到 `temporal.schedules`,UI 可见、支持 pause/backfill/skip |
|
|
333
|
+
|
|
334
|
+
---
|
|
335
|
+
|
|
336
|
+
### 场景 D:NestJS Public 端点(外部 webhook 触发,`webhook_trigger`)
|
|
337
|
+
|
|
338
|
+
> ✅ **当前状态**:已实现(HMAC 严格模式)。`POST /api/workflow/webhook/:path` 由 `WorkflowController.receiveWebhook` 接收,路由用 `@AllowUnauthorizedRequest()` 跳过登录态。请求必须带 `X-Workflow-Timestamp`(±5min)+ `X-Workflow-Signature: sha256=HEX(HMAC_SHA256(secret, "{ts}.{rawBody}"))`。验签失败 / 路径未匹配 / 时间戳过期分别返回 401 / 404 / 401。
|
|
339
|
+
|
|
340
|
+
入口:外部系统(如 GitHub / Stripe / 内部其他服务)按 Workflow 编辑器配置的 `webhookPath` 发 HTTP 请求 → NestJS 公开路由(无需登录态)匹配 published workflow 并复用 `executeWorkflow()` 流程。
|
|
341
|
+
|
|
342
|
+
```
|
|
343
|
+
┌──────────┐ ┌────────────────────┐ ┌──────────────────┐ ┌─────────────────┐ ┌────────────────┐ ┌────────────┐
|
|
344
|
+
│ 外部系统 │ │ NestJS │ │ NestJS │ │ PostgreSQL │ │ Temporal Server│ │ Worker │
|
|
345
|
+
│ (任何 HTTP│ │ WebhookController │ │ WorkflowService │ │ (业务 schema) │ │ (gRPC :7233) │ │ (long-poll)│
|
|
346
|
+
│ 客户端) │ │ @Public │ │ │ │ │ │ │ │ │
|
|
347
|
+
└────┬─────┘ └──────────┬─────────┘ └────────┬─────────┘ └────────┬────────┘ └───────┬────────┘ └─────┬──────┘
|
|
348
|
+
│ POST /api/workflow/webhook/:path │ │ │ │
|
|
349
|
+
│ Headers: X-Signature, body=payload │ │ │ │
|
|
350
|
+
├──────────────────►│ │ │ │ │
|
|
351
|
+
│ │ (可选) 校验签名 / IP 白名单 │ │ │
|
|
352
|
+
│ │ │ │ │ │
|
|
353
|
+
│ │ SELECT workflow JOIN version 找含 webhook_trigger 节点 │ │
|
|
354
|
+
│ │ 且 config.webhookPath = :path 的最新发布版本 │ │
|
|
355
|
+
│ ├──────────────────────────────────────────────►│ │ │
|
|
356
|
+
│ │◄──────── 0 / 1 / N 个匹配 workflow ───────────┤ │ │
|
|
357
|
+
│ │ │ │ │ │
|
|
358
|
+
│ │ 对每个匹配的 workflow(通常仅 1 个): │ │ │
|
|
359
|
+
│ │ executeWorkflow(workflowId, { context: { headers, query, body } }, creator='webhook') │
|
|
360
|
+
│ ├──────────────────────►│ │ │ │
|
|
361
|
+
│ │ │ INSERT t_workflow_instance (creator='webhook', context=请求体) │
|
|
362
|
+
│ │ ├─────────────────────►│ │ │
|
|
363
|
+
│ │ │◄────── instanceId ───┤ │ │
|
|
364
|
+
│ │ │ │ │ │
|
|
365
|
+
│ │ │ temporalService.startWorkflow('dslWorkflow', …) │
|
|
366
|
+
│ │ ├──────────────────────────────────────────►│ │
|
|
367
|
+
│ │ │ │ 写 executions / history_node │
|
|
368
|
+
│ │ │ │ + 投 WorkflowTask │ │
|
|
369
|
+
│ │ │◄────── runId ────────────────────────────┤ │
|
|
370
|
+
│ │ │ UPDATE t_workflow_instance SET temporal_run_id│ │
|
|
371
|
+
│ │ ├─────────────────────►│ │ │
|
|
372
|
+
│ │◄── { instanceId } ────┤ │ │ PollWorkflowTask │
|
|
373
|
+
│ │ │ │ │◄──────────────────┤
|
|
374
|
+
│◄── 202 Accepted { instanceIds } ──────────┤ │ ├── task ──────────►│
|
|
375
|
+
│ (异步:webhook 立刻返回,工作流后台跑) │ │ │ │ 执行 dslWorkflow
|
|
376
|
+
│ │ │ │ INSERT/UPDATE t_workflow_node_execution│
|
|
377
|
+
│ │ │ │◄──────────────────────────────────────┤
|
|
378
|
+
│ │ │ │ UPDATE t_workflow_instance (status) │
|
|
379
|
+
│ │ │ │◄──────────────────────────────────────┤
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
**关键设计点:**
|
|
383
|
+
|
|
384
|
+
| 关注点 | 推荐做法 |
|
|
385
|
+
|--------|----------|
|
|
386
|
+
| 鉴权 | NestJS Controller 用 `@Public()` 跳过登录态,但每个 workflow 自带 `webhookSecret`,请求需带 `X-Signature: HMAC_SHA256(secret, body)` |
|
|
387
|
+
| 路径冲突 | `webhookPath` 必须在 published workflow 间唯一(Publish 时校验冲突,或用 `wf_${workflowId}` 做后缀) |
|
|
388
|
+
| 同步 vs 异步 | 默认异步:返回 `202 Accepted { instanceId }`,由前端轮询 `/instance/:id` 看结果。同步模式可选:等到 workflow 完成再返回(注意网关超时) |
|
|
389
|
+
| 多 workflow 订阅同一路径 | 与 outbox 一致:并行启动多个 instance,各自独立 |
|
|
390
|
+
| 重放保护 | 校验 `X-Timestamp` 在 ±5min 内 + payload 哈希做去重(可选,存 Redis 5min TTL) |
|
|
391
|
+
|
|
392
|
+
**涉及的 PG 表:**
|
|
393
|
+
|
|
394
|
+
| 表 | 操作 | 写入方 |
|
|
395
|
+
|----|------|--------|
|
|
396
|
+
| `t_workflow`、`t_workflow_version` | SELECT 含 `webhook_trigger` 节点的最新发布版本 | NestJS `WebhookController` |
|
|
397
|
+
| `t_workflow_instance` | INSERT (creator='webhook') → UPDATE temporal_run_id → UPDATE status | NestJS 创建;Worker `reporting.ts` 改状态 |
|
|
398
|
+
| `t_workflow_node_execution` | INSERT/UPDATE | Worker `reporting.ts` |
|
|
399
|
+
| `temporal.executions` / `history_node` / `task_queues` | 同场景 A | Temporal Server |
|
|
400
|
+
|
|
401
|
+
**为什么不让 Worker 自己开 HTTP 端口接收 webhook?**
|
|
402
|
+
|
|
403
|
+
- Worker 是无状态计算节点,不应面向公网;外部网络/网关/WAF 都暴露在 NestJS 那一层
|
|
404
|
+
- NestJS 已有完善的 Guard、Logger、Swagger、错误处理体系,复用更合适
|
|
405
|
+
- 校验签名、限流、IP 白名单等都属于"边界关注点",归属 Web 层
|
|
406
|
+
|
|
407
|
+
---
|
|
408
|
+
|
|
409
|
+
### 一句话对比(四种触发场景)
|
|
410
|
+
|
|
411
|
+
| | 场景 A:NestJS startWorkflow | 场景 B:Outbox Poller | 场景 C:Temporal Schedule | 场景 D:NestJS Webhook |
|
|
412
|
+
|--|------------------------------|------------------------|----------------------------|--------------------------|
|
|
413
|
+
| **当前状态** | ✅ 已实现 | ✅ 已实现 | ✅ 已实现 | ✅ 已实现(HMAC 严格) |
|
|
414
|
+
| 触发点 | 用户/定时业务调用 NestJS | 业务表 DML → 触发器 → outbox | Server 内置 cron 调度 | 外部 HTTP POST |
|
|
415
|
+
| 入口节点 | (任何,无 trigger 节点) | `event_trigger` | `cron_trigger` | `webhook_trigger` |
|
|
416
|
+
| 谁创建 `t_workflow_instance` | NestJS `WorkflowService` | Worker `outbox-poller` | Worker(cronTriggerWorkflow 内 Activity) | NestJS `WorkflowService` |
|
|
417
|
+
| 谁调用 `workflow.start()` | NestJS `TemporalService` | Worker `outbox-poller` | **Temporal Server 自己**(Schedule 系统) | NestJS `TemporalService` |
|
|
418
|
+
| 是否走 Temporal Server | ✅ | ✅ | ✅(且 schedule 本身也由 Server 持久化) | ✅ |
|
|
419
|
+
| `creator` 字段 | 登录用户 | `'event_trigger'` | `'cron'` | `'webhook'` |
|
|
420
|
+
| 触发延迟 | <100ms | ≤ 轮询间隔(默认 10s) | 由 Server 内部 timer 控制(秒级精度) | <100ms |
|
|
421
|
+
| 失败重试 | Workflow/Activity 自带重试 | outbox 行重试 ≤ `OUTBOX_MAX_RETRIES` | Schedule 不重试触发;Workflow 内部正常重试 | NestJS 直接返回 4xx/5xx;webhook 客户端自己决定是否重投 |
|
|
422
|
+
| 多实例并发安全 | NestJS 多 Pod 各自 OK | `FOR UPDATE SKIP LOCKED` 防重复消费 | Server 单一 schedule 实例,天然不重复 | NestJS 多 Pod 各自 OK |
|
|
423
|
+
|
|
424
|
+
**关键点:** 不论从哪个入口触发,只要调用了 `temporalClient.workflow.start()`(或 Schedule 间接调用),就一定要经过 Temporal Server 的调度(写 history → 入 task queue → Worker long-poll 拉取),Server 是**唯一的状态权威**。Worker 内部直接读写业务表只发生在 Activity 执行阶段(`reporting.ts` 写 `t_workflow_node_execution` / `t_workflow_instance`,outbox-poller 写 outbox 与 instance),这是绕不过 Server 的——但写 Workflow 历史和调度本身仍由 Server 完成。
|
|
425
|
+
|
|
123
426
|
## 环境变量
|
|
124
427
|
|
|
125
428
|
| 变量 | 默认值 | 说明 |
|
|
@@ -213,10 +516,16 @@ npm run worker:start # 生产模式启动 Worker
|
|
|
213
516
|
|
|
214
517
|
**Step 1:创建 outbox 表 + 通用触发器函数(只需执行一次)**
|
|
215
518
|
|
|
519
|
+
Outbox 表 `t_workflow_event_outbox` 由 Prisma 模型 `WorkflowEventOutbox` 维护,跟随 `prisma migrate` 流程自动建表;通用触发器函数 `notify_workflow_event()` 和未处理事件的部分索引 `idx_outbox_unprocessed` 由 seed 脚本创建:
|
|
520
|
+
|
|
216
521
|
```bash
|
|
217
|
-
|
|
522
|
+
# server 目录
|
|
523
|
+
cd server
|
|
524
|
+
yarn seed
|
|
218
525
|
```
|
|
219
526
|
|
|
527
|
+
幂等可重复执行。详见 `server/seed/workflow-event-trigger.ts`。
|
|
528
|
+
|
|
220
529
|
**Step 2:给需要监听的表挂触发器**
|
|
221
530
|
|
|
222
531
|
```sql
|
|
@@ -0,0 +1,152 @@
|
|
|
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
|
+
```
|
|
@@ -0,0 +1,79 @@
|
|
|
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"]
|