camunda-python 0.1.0__py3-none-any.whl
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.
- camunda/__init__.py +10 -0
- camunda/api/__init__.py +14 -0
- camunda/api/app.py +80 -0
- camunda/api/deps.py +17 -0
- camunda/api/errors.py +84 -0
- camunda/api/pagination.py +74 -0
- camunda/api/routers/__init__.py +5 -0
- camunda/api/routers/decision.py +57 -0
- camunda/api/routers/deployment.py +139 -0
- camunda/api/routers/history.py +128 -0
- camunda/api/routers/process_definition.py +49 -0
- camunda/api/routers/process_instance.py +106 -0
- camunda/api/routers/task.py +92 -0
- camunda/api/schemas.py +200 -0
- camunda/common/__init__.py +19 -0
- camunda/common/clock.py +30 -0
- camunda/common/exceptions.py +34 -0
- camunda/common/idgen.py +23 -0
- camunda/common/timers.py +106 -0
- camunda/dmn/__init__.py +5 -0
- camunda/dmn/engine.py +219 -0
- camunda/dmn/feel.py +392 -0
- camunda/engine/__init__.py +9 -0
- camunda/engine/behavior.py +51 -0
- camunda/engine/expression.py +126 -0
- camunda/engine/process_engine.py +3237 -0
- camunda/job/__init__.py +9 -0
- camunda/job/executor.py +136 -0
- camunda/model/__init__.py +48 -0
- camunda/model/bpmn.py +326 -0
- camunda/model/dmn.py +101 -0
- camunda/model/execution.py +121 -0
- camunda/model/job.py +88 -0
- camunda/model/task.py +33 -0
- camunda/model/variable.py +35 -0
- camunda/parser/__init__.py +5 -0
- camunda/parser/bpmn_parser.py +646 -0
- camunda/parser/dmn_parser.py +225 -0
- camunda/persistence/__init__.py +21 -0
- camunda/persistence/entities.py +202 -0
- camunda/persistence/store.py +721 -0
- camunda_python-0.1.0.dist-info/METADATA +377 -0
- camunda_python-0.1.0.dist-info/RECORD +46 -0
- camunda_python-0.1.0.dist-info/WHEEL +5 -0
- camunda_python-0.1.0.dist-info/licenses/LICENSE +200 -0
- camunda_python-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,3237 @@
|
|
|
1
|
+
"""ProcessEngine 门面:流程引擎(M1 内存 / M2 可选持久化 / M3 作业)。
|
|
2
|
+
|
|
3
|
+
架构对齐(见 docs/ARCHITECTURE.md):
|
|
4
|
+
- RepositoryService 语义 -> deploy() / get_process_definition()
|
|
5
|
+
- RuntimeService 语义 -> start_process_instance_by_key() / get_process_instance()
|
|
6
|
+
- TaskService 语义 -> complete_task() / create_task_query()
|
|
7
|
+
|
|
8
|
+
执行模型(语义对齐 Camunda PVM 的核心):
|
|
9
|
+
- **Execution 树**:实例根 Execution 下按需挂子 Execution。并行网关 fork 时
|
|
10
|
+
父 execution 转 SCOPE 停驻,每条出边 spawn 一个子 TOKEN execution。
|
|
11
|
+
- **队列推进**:token 到达节点产生新到达事件,入队循环 pump,天然支撑并发。
|
|
12
|
+
- **并行网关 join**:实例级 join_arrivals[gw_id] 登记到达 token 数;到达数 ==
|
|
13
|
+
网关入边数 时汇聚:清停等 token、SCOPE 恢复沿网关出边继续。
|
|
14
|
+
(M1 约束:无循环回连并行网关、并行分支内不嵌套并行,M4 强化。)
|
|
15
|
+
- **变量作用域**:全放 ProcessInstance.variables(实例级)。
|
|
16
|
+
- **活动历史**:ActivityInstance 进/出痕迹(内存 + M2 落 ACT_HI_ACTINST)。
|
|
17
|
+
|
|
18
|
+
M2 持久化(事务边界同步):
|
|
19
|
+
- deploy / start / complete 三个命令结束后把实例状态全量同步到 ACT 表;
|
|
20
|
+
进程崩溃 => 未完成命令整体丢失(等价 Camunda 单命令事务语义)。
|
|
21
|
+
- from_database() 从 ACT_RU_* 恢复活跃实例(execution 树/task/变量/join 等待)。
|
|
22
|
+
|
|
23
|
+
M3 作业(Job / Timer / async continuation):
|
|
24
|
+
- token 到达 timer 中间捕获事件 -> 停等 + 注册 timer-catch Job(duedate)-> 到期
|
|
25
|
+
execute_job 让 token 继续;Timer Start 为定义级 timer-start Job,触发即启动实例。
|
|
26
|
+
- camunda:asyncBefore 把节点行为执行拆成 async-continuation Job(Camunda async
|
|
27
|
+
continuation 语义);execute_job 失败 -> retries-1 -> 到期重试;retries 耗尽 =
|
|
28
|
+
死信(不再 acquire,实例级失败时若启用 store 自动回滚内存到上次同步点)。
|
|
29
|
+
|
|
30
|
+
M4-1 扩展:
|
|
31
|
+
- timer 边界事件(timer-boundary Job):宿主 = 有等待点的活动
|
|
32
|
+
(userTask / asyncBefore 节点)。中断式(cancelActivity=true)等待期内到点
|
|
33
|
+
触发即取消宿主、token 改走边界事件出边;非中断式(cancelActivity=false,
|
|
34
|
+
M4-2b4 落地)到点触发不取消宿主,spawn 并发线从边界事件出边走(单发,宿主
|
|
35
|
+
继续等待,可多次触发不同边界/同一边界不同窗口——timeCycle 不支持,文档化
|
|
36
|
+
差异)。宿主正常离开撤销边界 Job。
|
|
37
|
+
- camunda:asyncAfter 把「离开推进」拆成 async-after Job(支持 serviceTask /
|
|
38
|
+
exclusiveGateway;XOR 离开时重新求值出边条件;其余类型明确报错——文档化差异)。
|
|
39
|
+
可与 asyncBefore 链式。
|
|
40
|
+
- camunda:asyncAfter 把「离开推进」拆成 async-after Job(支持 serviceTask /
|
|
41
|
+
exclusiveGateway;XOR 离开时重新求值出边条件;其余类型明确报错——文档化差异)。
|
|
42
|
+
可与 asyncBefore 链式。
|
|
43
|
+
|
|
44
|
+
M4-2a 扩展(embedded SubProcess 容器语义):
|
|
45
|
+
- 容器感知流转:token 可能在内嵌子流程内部推进,节点/连线/边界作业归属一律按
|
|
46
|
+
token 所在容器(_container_of:沿父链找最近停驻在 SubProcess 的 SCOPE 祖先,
|
|
47
|
+
取其 inner Process;否则根 Process)解析,跨容器节点 id 不串扰。
|
|
48
|
+
- 进入 subProcess:token 转 SCOPE 停驻(activity=subProcess id,actinst 跨整段
|
|
49
|
+
内部执行期 open),spawn 内部子 token 从内部 startEvent 推进。
|
|
50
|
+
- 收束复活:内部全部走完(含并行分支直通 end 的逐层 SCOPE 收束)后,subProcess
|
|
51
|
+
SCOPE 无活跃子 -> 结算 actinst、恢复 TOKEN 沿 sub 出边继续;并行 join 汇聚后
|
|
52
|
+
恢复的 SCOPE 立即复位 TOKEN,避免被收束扫描误杀。
|
|
53
|
+
- 边界 timer 中断子流程(M4-2a3):进入 subProcess 时注册其边界作业;触发即
|
|
54
|
+
取消整段 scope(内部子树全部结束:execution ENDED、任务归档、作业删除、
|
|
55
|
+
actinst 结算、join 登记摘除),token 改走边界事件出边。
|
|
56
|
+
- 约束(文档化差异):任何容器内并行分支路径不嵌套并行网关(沿用 M1 约束);
|
|
57
|
+
subProcess 的 asyncAfter 不支持(asyncBefore 支持,语义 = 展开前异步窗口);
|
|
58
|
+
非中断式边界(cancelActivity=false)M4-2b4 起支持普通等待活动宿主,subProcess
|
|
59
|
+
宿主不支持(中断式支持,见 M4-2a3)。
|
|
60
|
+
|
|
61
|
+
M4-2b 扩展(事件子流程 + 错误传播):
|
|
62
|
+
- 事件子流程(triggeredByEvent=true)不参与 sequenceFlow,由内部事件 start
|
|
63
|
+
触发:error start(中断式)/ timer start(中断+非中断)/ message start
|
|
64
|
+
(解析保留,消息投递入口未实现前订阅即明确报错——文档化差异)。
|
|
65
|
+
- 订阅生命周期对齐 Camunda:宿主 scope(流程实例或 subProcess)激活时建立
|
|
66
|
+
触发条件(error = 冒泡匹配;timer = 注册实例级 timer-event-start Job),
|
|
67
|
+
宿主 scope 结束即失效。
|
|
68
|
+
- error endEvent 抛出错误:沿宿主 scope 链(内到外:所在容器 -> 上级 subProcess
|
|
69
|
+
-> 流程实例)冒泡找匹配的 error 事件子流程;命中 -> 中断宿主 scope 其它执行
|
|
70
|
+
(根容器=整实例、subProcess 容器=该 subProcess 内部),事件子流程成为宿主
|
|
71
|
+
scope 下唯一活动,走完即宿主 scope 结束(根=实例完成 / subProcess=正常复活
|
|
72
|
+
沿出边继续);无命中 -> 等同 none end(仅当前路径结束,文档化差异对齐 Camunda
|
|
73
|
+
error end event 默认语义)。
|
|
74
|
+
- 事件子流程 scope 建模:SCOPE execution 停驻在事件子流程节点(activity_id=
|
|
75
|
+
事件子流程 id),与 embedded SubProcess 同一收束/容器推导路径,零表改动。
|
|
76
|
+
- 非中断式边界事件(cancelActivity=false,M4-2b4 落地):宿主等待期内触发
|
|
77
|
+
不取消宿主,spawn 并发线从边界事件出边走;主线与并发线全收束才算实例完成
|
|
78
|
+
(root 到 end 不再等价实例完成——root 转 SCOPE 停驻等并发子树收束后收尾)。
|
|
79
|
+
|
|
80
|
+
时间来自可注入时钟 camunda.common.clock(测试拨时间无需真等待)。所有命令入口
|
|
81
|
+
持引擎级 RLock,JobExecutor 轮询线程与用户命令不会互踩(多进程部署锁不在范围)。
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
from __future__ import annotations
|
|
85
|
+
|
|
86
|
+
import logging
|
|
87
|
+
import threading
|
|
88
|
+
from collections import deque
|
|
89
|
+
from dataclasses import dataclass
|
|
90
|
+
from datetime import datetime, timedelta
|
|
91
|
+
from typing import TYPE_CHECKING, Any, Callable, Deque, Dict, List, Optional, Tuple
|
|
92
|
+
|
|
93
|
+
if TYPE_CHECKING: # Store 仅持久化模式实例化,纯内存模式不强依赖 sqlalchemy
|
|
94
|
+
from camunda.persistence.store import ProcInstSnap, Store
|
|
95
|
+
|
|
96
|
+
from camunda.common import clock
|
|
97
|
+
from camunda.common.exceptions import (
|
|
98
|
+
InvalidRequestException,
|
|
99
|
+
NotFoundException,
|
|
100
|
+
ProcessInstanceException,
|
|
101
|
+
)
|
|
102
|
+
from camunda.common.idgen import IdGenerator
|
|
103
|
+
from camunda.common.timers import (
|
|
104
|
+
format_iso,
|
|
105
|
+
next_trigger,
|
|
106
|
+
parse_iso,
|
|
107
|
+
parse_iso_repeat,
|
|
108
|
+
parse_trigger_date,
|
|
109
|
+
)
|
|
110
|
+
from camunda.engine.behavior import select_exclusive_gateway_flow
|
|
111
|
+
from camunda.engine.expression import evaluate_condition, evaluate_expression
|
|
112
|
+
from camunda.model.bpmn import (
|
|
113
|
+
BpmnModel,
|
|
114
|
+
BoundaryEvent,
|
|
115
|
+
EndEvent,
|
|
116
|
+
ExclusiveGateway,
|
|
117
|
+
BusinessRuleTask,
|
|
118
|
+
FlowNode,
|
|
119
|
+
IntermediateCatchEvent,
|
|
120
|
+
IntermediateThrowEvent,
|
|
121
|
+
MultiInstance,
|
|
122
|
+
ParallelGateway,
|
|
123
|
+
Process,
|
|
124
|
+
SequenceFlow,
|
|
125
|
+
ServiceTask,
|
|
126
|
+
StartEvent,
|
|
127
|
+
SubProcess,
|
|
128
|
+
UserTask,
|
|
129
|
+
)
|
|
130
|
+
from camunda.model.execution import (
|
|
131
|
+
ActivityInstance,
|
|
132
|
+
Execution,
|
|
133
|
+
ExecutionState,
|
|
134
|
+
ProcessInstance,
|
|
135
|
+
ProcessInstanceState,
|
|
136
|
+
)
|
|
137
|
+
from camunda.model.job import Job
|
|
138
|
+
from camunda.model.task import Task
|
|
139
|
+
from camunda.dmn.engine import DmnEngine
|
|
140
|
+
from camunda.model.dmn import DmnModel
|
|
141
|
+
|
|
142
|
+
# 到达事件:(token execution, 即将进入的节点)
|
|
143
|
+
_Arrival = Tuple[Execution, FlowNode]
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
@dataclass
|
|
147
|
+
class EventSubscription:
|
|
148
|
+
"""消息/信号事件订阅(M4-2d,纯内存派生态——不落库,恢复时重推导)。
|
|
149
|
+
|
|
150
|
+
挂载点(node_kind):
|
|
151
|
+
- start:事件子流程 message/signal start,execution = 宿主 scope
|
|
152
|
+
(activity_id = 订阅容器 subProcess id,None = 流程级);
|
|
153
|
+
- catch:IntermediateCatchEvent 停等 token,execution = 停等执行;
|
|
154
|
+
- boundary:宿主停等活动上的 message/signal 边界,execution = 宿主 token。
|
|
155
|
+
|
|
156
|
+
kind: "message" | "signal";is_interrupting:boundary 用 cancel_activity、
|
|
157
|
+
esc start 用 isInterrupting(catch 无中断概念恒 True 填充)。
|
|
158
|
+
生命周期:scope 激活即注册;触发即消费(catch/boundary-中断/esc-中断式)
|
|
159
|
+
或随宿主离开/杀灭/收束撤销;非中断式 boundary 与 esc-start 订阅常驻可再触发。
|
|
160
|
+
"""
|
|
161
|
+
|
|
162
|
+
id: str
|
|
163
|
+
kind: str # "message" | "signal"
|
|
164
|
+
event_name: str
|
|
165
|
+
process_instance_id: str
|
|
166
|
+
execution_id: str
|
|
167
|
+
activity_id: Optional[str] # esc 订阅容器 subProcess id(None = 流程级)
|
|
168
|
+
node_id: str # 事件节点 id(esc start / catch / boundary)
|
|
169
|
+
node_kind: str # "start" | "catch" | "boundary"
|
|
170
|
+
is_interrupting: bool
|
|
171
|
+
created: str
|
|
172
|
+
|
|
173
|
+
# 实现委托签名:callable(variables: dict) -> None | dict(merge 更新)
|
|
174
|
+
Delegate = Callable[[Dict[str, Any]], Optional[Dict[str, Any]]]
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _now() -> str:
|
|
178
|
+
"""当前时间(定长 ISO,本地时区)。走可注入时钟,测试可拨快。"""
|
|
179
|
+
return clock.now()
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
# M3:单条作业执行失败不阻塞整轮轮询(对齐 Camunda JobExecutor 行为)
|
|
183
|
+
logger = logging.getLogger(__name__)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
class ProcessEngine:
|
|
187
|
+
"""流程引擎(M1 内存版 / M2 可选持久化 / M3 作业)。
|
|
188
|
+
|
|
189
|
+
M2/M3 用法:
|
|
190
|
+
from camunda.persistence.store import Store
|
|
191
|
+
engine = ProcessEngine(store=Store("sqlite:///camunda.db")) # 启用落库
|
|
192
|
+
engine = ProcessEngine.from_database("sqlite:///camunda.db") # 崩溃恢复
|
|
193
|
+
engine.execute_due_jobs() # 手动触发到期作业(JobExecutor 轮询也是调它)
|
|
194
|
+
engine.create_job_query() # 查看作业(待办/死信)
|
|
195
|
+
|
|
196
|
+
未传 store 时行为与 M1 完全一致(纯内存,既有测试不破坏)。
|
|
197
|
+
多进程部署抢锁不在 M3 范围(见 docs/ARCHITECTURE.md 风险章节)。
|
|
198
|
+
"""
|
|
199
|
+
|
|
200
|
+
def __init__(self, store: Optional["Store"] = None) -> None:
|
|
201
|
+
# key -> Process(同名重复部署视为新版本,覆盖并版本+1)
|
|
202
|
+
self._definitions: Dict[str, Process] = {}
|
|
203
|
+
self._definition_versions: Dict[str, int] = {}
|
|
204
|
+
# M6:key -> 原始 BPMN XML(供 REST GET /process-definition/{key}/xml)
|
|
205
|
+
self._definition_sources: Dict[str, Optional[str]] = {}
|
|
206
|
+
self._instances: Dict[str, ProcessInstance] = {}
|
|
207
|
+
self._tasks: Dict[str, Task] = {}
|
|
208
|
+
# 实现注册表:serviceTask implementation_ref -> callable
|
|
209
|
+
self._delegates: Dict[str, Delegate] = {}
|
|
210
|
+
self._idgen = IdGenerator()
|
|
211
|
+
# M2:持久化 store(None = 纯内存模式)
|
|
212
|
+
self._store = store
|
|
213
|
+
# M3:作业池(实例级 timer-catch/async + 定义级 timer-start)
|
|
214
|
+
self._jobs: Dict[str, Job] = {}
|
|
215
|
+
# M4-2d:消息/信号订阅池(插入序 = 注册序,事件订阅检索按序取最早)
|
|
216
|
+
self._event_subs: Dict[str, EventSubscription] = {}
|
|
217
|
+
# M5:DMN 决策引擎(部署不落库,对齐 delegate 注册先例)
|
|
218
|
+
self._dmn = DmnEngine()
|
|
219
|
+
# M6:部署记录(id/name/time/keys,供 REST GET /deployment 列举)
|
|
220
|
+
self._deployments: List[Dict[str, Any]] = []
|
|
221
|
+
# 命令级互斥:JobExecutor 轮询线程与用户命令入口共用
|
|
222
|
+
self._lock = threading.RLock()
|
|
223
|
+
|
|
224
|
+
# ------------------------------------------------------------------
|
|
225
|
+
# 委托注册(对齐 Spring bean / JavaDelegate 注册语义)
|
|
226
|
+
# ------------------------------------------------------------------
|
|
227
|
+
def register_delegate(self, name: str, fn: Delegate) -> None:
|
|
228
|
+
"""注册 serviceTask 实现。fn(variables) 原地改或返回 dict 合并。"""
|
|
229
|
+
with self._lock:
|
|
230
|
+
if not callable(fn):
|
|
231
|
+
raise InvalidRequestException(f"delegate {name!r} 必须可调用")
|
|
232
|
+
self._delegates[name] = fn
|
|
233
|
+
|
|
234
|
+
# ------------------------------------------------------------------
|
|
235
|
+
# RepositoryService 语义
|
|
236
|
+
# ------------------------------------------------------------------
|
|
237
|
+
def deploy(self, model: BpmnModel, name: Optional[str] = None) -> List[str]:
|
|
238
|
+
"""部署 BpmnModel,返回部署成功的 process key 列表。
|
|
239
|
+
|
|
240
|
+
重复部署同名 key:覆盖并版本 +1(对齐 ACT_RE_PROCDEF 多版本语义)。
|
|
241
|
+
M2:启用 store 时同步写 ACT_RE_*(含原始 xml 供恢复重解析)。
|
|
242
|
+
M3:带 timer start 的流程注册定义级 timer-start 作业(新版本覆盖旧版作业组)。
|
|
243
|
+
"""
|
|
244
|
+
with self._lock:
|
|
245
|
+
keys: List[str] = []
|
|
246
|
+
for proc in model.processes:
|
|
247
|
+
if not proc.is_executable:
|
|
248
|
+
continue
|
|
249
|
+
self._definitions[proc.id] = proc
|
|
250
|
+
self._definition_versions[proc.id] = (
|
|
251
|
+
self._definition_versions.get(proc.id, 0) + 1
|
|
252
|
+
)
|
|
253
|
+
self._definition_sources[proc.id] = model.source_xml
|
|
254
|
+
keys.append(proc.id)
|
|
255
|
+
# 新版本取代旧版本 -> 该 key 的旧 timer-start 作业组整体移除重建
|
|
256
|
+
self._drop_definition_jobs(proc.id)
|
|
257
|
+
for start in proc.start_events:
|
|
258
|
+
if start.timer is not None:
|
|
259
|
+
job = self._make_timer_start_job(proc.id, start)
|
|
260
|
+
self._jobs[job.id] = job
|
|
261
|
+
if keys:
|
|
262
|
+
# 落库时复用 store 生成的部署 id;纯内存用 idgen
|
|
263
|
+
dep_id = (
|
|
264
|
+
self._store.save_deployment(model, _now())
|
|
265
|
+
if self._store is not None
|
|
266
|
+
else self._idgen.next_id()
|
|
267
|
+
)
|
|
268
|
+
if self._store is not None:
|
|
269
|
+
self._sync_timer_start_jobs()
|
|
270
|
+
self._deployments.append(
|
|
271
|
+
{
|
|
272
|
+
"id": dep_id,
|
|
273
|
+
"name": name,
|
|
274
|
+
"time": _now(),
|
|
275
|
+
"source": model.source_name,
|
|
276
|
+
"process_keys": list(keys),
|
|
277
|
+
"decision_keys": [],
|
|
278
|
+
}
|
|
279
|
+
)
|
|
280
|
+
return keys
|
|
281
|
+
|
|
282
|
+
def get_process_definition(self, key: str) -> Process:
|
|
283
|
+
with self._lock:
|
|
284
|
+
if key not in self._definitions:
|
|
285
|
+
raise NotFoundException(f"未部署的流程定义: {key!r}")
|
|
286
|
+
return self._definitions[key]
|
|
287
|
+
|
|
288
|
+
def get_definition_version(self, key: str) -> int:
|
|
289
|
+
return self._definition_versions.get(key, 0)
|
|
290
|
+
|
|
291
|
+
# ------------------------------------------------------------------
|
|
292
|
+
# DecisionService 语义(M5:DMN 决策)
|
|
293
|
+
# ------------------------------------------------------------------
|
|
294
|
+
def deploy_dmn(self, model: DmnModel, name: Optional[str] = None) -> List[str]:
|
|
295
|
+
"""部署 DmnModel,返回 decision key 列表(重复 key 版本 +1)。
|
|
296
|
+
|
|
297
|
+
注意:DMN 部署不落库(文档化差异,对齐 delegate 注册不落库)——
|
|
298
|
+
崩溃恢复后须重新 deploy_dmn,否则 businessRuleTask 求值报未部署。
|
|
299
|
+
"""
|
|
300
|
+
with self._lock:
|
|
301
|
+
keys = self._dmn.deploy(model)
|
|
302
|
+
if keys:
|
|
303
|
+
self._deployments.append(
|
|
304
|
+
{
|
|
305
|
+
"id": self._idgen.next_id(),
|
|
306
|
+
"name": name,
|
|
307
|
+
"time": _now(),
|
|
308
|
+
"source": model.source_name,
|
|
309
|
+
"process_keys": [],
|
|
310
|
+
"decision_keys": list(keys),
|
|
311
|
+
}
|
|
312
|
+
)
|
|
313
|
+
return keys
|
|
314
|
+
|
|
315
|
+
def evaluate_decision(self, key: str, variables: Optional[Dict[str, Any]] = None) -> Any:
|
|
316
|
+
"""直接求值已部署决策(对齐 DecisionService.evaluateDecisionTable)。"""
|
|
317
|
+
with self._lock:
|
|
318
|
+
return self._dmn.evaluate_decision(key, variables)
|
|
319
|
+
|
|
320
|
+
def get_decision_definition(self, key: str):
|
|
321
|
+
"""按 key 取已部署决策(未部署抛 NotFoundException)。"""
|
|
322
|
+
with self._lock:
|
|
323
|
+
return self._dmn.get_decision(key)
|
|
324
|
+
|
|
325
|
+
def get_decision_version(self, key: str) -> int:
|
|
326
|
+
return self._dmn.get_decision_version(key)
|
|
327
|
+
|
|
328
|
+
# ------------------------------------------------------------------
|
|
329
|
+
# RuntimeService 语义
|
|
330
|
+
# ------------------------------------------------------------------
|
|
331
|
+
def start_process_instance_by_key(
|
|
332
|
+
self,
|
|
333
|
+
process_key: str,
|
|
334
|
+
variables: Optional[Dict[str, Any]] = None,
|
|
335
|
+
business_key: Optional[str] = None,
|
|
336
|
+
) -> ProcessInstance:
|
|
337
|
+
"""按 key 启动实例:建树 -> startEvent token 入队 pump。
|
|
338
|
+
|
|
339
|
+
定时启动流程(startEvent 带 timer)不可手动启动,对齐 Camunda 语义。
|
|
340
|
+
"""
|
|
341
|
+
with self._lock:
|
|
342
|
+
proc = self.get_process_definition(process_key)
|
|
343
|
+
if not proc.start_events:
|
|
344
|
+
raise ProcessInstanceException(f"流程 {process_key!r} 没有可启动的 startEvent")
|
|
345
|
+
start = proc.start_events[0] # M1:取第一个 startEvent
|
|
346
|
+
if start.timer is not None:
|
|
347
|
+
raise ProcessInstanceException(
|
|
348
|
+
f"流程 {process_key!r} 是定时启动流程(startEvent {start.id} 带 timer),不能手动启动"
|
|
349
|
+
)
|
|
350
|
+
if start.error_code is not None or start.message_name is not None or start.signal_name is not None:
|
|
351
|
+
raise ProcessInstanceException(
|
|
352
|
+
f"流程 {process_key!r} 的 startEvent {start.id} 是事件 start"
|
|
353
|
+
"(error/message/signal):流程级事件启动后续里程碑落地,不能手动启动"
|
|
354
|
+
)
|
|
355
|
+
return self._start_process(proc, variables, business_key, start)
|
|
356
|
+
|
|
357
|
+
def _start_process(
|
|
358
|
+
self,
|
|
359
|
+
proc: Process,
|
|
360
|
+
variables: Optional[Dict[str, Any]],
|
|
361
|
+
business_key: Optional[str],
|
|
362
|
+
start: Optional[StartEvent] = None,
|
|
363
|
+
) -> ProcessInstance:
|
|
364
|
+
"""内部启动:手动与 timer-start 共用。timer 触发时传入对应 startEvent。"""
|
|
365
|
+
start = start or proc.start_events[0]
|
|
366
|
+
pi = ProcessInstance(
|
|
367
|
+
id=self._idgen.next_id(),
|
|
368
|
+
process_definition_key=proc.id,
|
|
369
|
+
business_key=business_key,
|
|
370
|
+
variables=dict(variables or {}),
|
|
371
|
+
start_time=_now(),
|
|
372
|
+
)
|
|
373
|
+
root = Execution(id=self._idgen.next_id(), process_instance_id=pi.id)
|
|
374
|
+
pi.root_execution = root
|
|
375
|
+
pi.executions[root.id] = root
|
|
376
|
+
self._instances[pi.id] = pi
|
|
377
|
+
|
|
378
|
+
self._pump(pi, [(root, start)])
|
|
379
|
+
# M4-2b3:流程实例 scope 激活 -> 订阅根 Process 容器内 timer 事件子流程
|
|
380
|
+
# (root 直通进入 sub 时同样成立——容器由注册点显式给出,不与 sub 订阅混淆)
|
|
381
|
+
if not pi.is_completed:
|
|
382
|
+
self._register_event_subprocess_timers(pi, root, proc, None)
|
|
383
|
+
# M4-2d:流程级容器 message/signal esc start 常驻订阅
|
|
384
|
+
self._register_event_subprocess_subscriptions(pi, root, proc, None)
|
|
385
|
+
if self._store is not None:
|
|
386
|
+
self._store.save_proc_inst(self._build_snap(pi))
|
|
387
|
+
return pi
|
|
388
|
+
|
|
389
|
+
def get_process_instance(self, instance_id: str) -> ProcessInstance:
|
|
390
|
+
with self._lock:
|
|
391
|
+
if instance_id not in self._instances:
|
|
392
|
+
raise NotFoundException(f"流程实例不存在: {instance_id!r}")
|
|
393
|
+
return self._instances[instance_id]
|
|
394
|
+
|
|
395
|
+
def list_process_instances(self) -> List[ProcessInstance]:
|
|
396
|
+
with self._lock:
|
|
397
|
+
return list(self._instances.values())
|
|
398
|
+
|
|
399
|
+
# ------------------------------------------------------------------
|
|
400
|
+
# M6 补充:REST 需要的门面能力(定义列表 / 删除实例 / 任务认领)
|
|
401
|
+
# ------------------------------------------------------------------
|
|
402
|
+
def list_process_definitions(self) -> List[Dict[str, Any]]:
|
|
403
|
+
"""已部署流程定义列表(key / name / version,部署序)。"""
|
|
404
|
+
with self._lock:
|
|
405
|
+
return [
|
|
406
|
+
{
|
|
407
|
+
"key": key,
|
|
408
|
+
"name": proc.name,
|
|
409
|
+
"version": self._definition_versions.get(key, 0),
|
|
410
|
+
}
|
|
411
|
+
for key, proc in self._definitions.items()
|
|
412
|
+
]
|
|
413
|
+
|
|
414
|
+
def get_process_definition_xml(self, key: str) -> Optional[str]:
|
|
415
|
+
"""流程定义原始 XML(部署时未带 source_xml 则为 None)。"""
|
|
416
|
+
with self._lock:
|
|
417
|
+
if key not in self._definitions:
|
|
418
|
+
raise NotFoundException(f"未部署的流程定义: {key!r}")
|
|
419
|
+
return self._definition_sources.get(key)
|
|
420
|
+
|
|
421
|
+
def list_deployments(self) -> List[Dict[str, Any]]:
|
|
422
|
+
"""部署记录列表(部署序,M6 供 REST GET /deployment 使用)。"""
|
|
423
|
+
with self._lock:
|
|
424
|
+
return list(self._deployments)
|
|
425
|
+
|
|
426
|
+
def list_decision_definitions(self) -> List[Dict[str, Any]]:
|
|
427
|
+
"""已部署决策定义列表(key / name / version,部署序)。"""
|
|
428
|
+
with self._lock:
|
|
429
|
+
return self._dmn.list_decisions()
|
|
430
|
+
|
|
431
|
+
def delete_process_instance(
|
|
432
|
+
self, instance_id: str, reason: Optional[str] = None
|
|
433
|
+
) -> None:
|
|
434
|
+
"""删除流程实例(运行中亦可):清内存态 + RU 行,历史保留。
|
|
435
|
+
|
|
436
|
+
对齐 Camunda 默认语义:不传 skipHistory 时历史行保留(HI_PROCINST 置
|
|
437
|
+
DELETED)。已结束实例同样可删(幂等清理运行时残渣,历史不动)。
|
|
438
|
+
"""
|
|
439
|
+
with self._lock:
|
|
440
|
+
pi = self._instances.get(instance_id)
|
|
441
|
+
if pi is None:
|
|
442
|
+
raise NotFoundException(f"流程实例不存在: {instance_id!r}")
|
|
443
|
+
# 1) 该实例的任务全部下线(活跃任务不入历史归档,删实例非正常完成)
|
|
444
|
+
for tid in [t.id for t in self._tasks.values()
|
|
445
|
+
if t.process_instance_id == instance_id]:
|
|
446
|
+
self._tasks.pop(tid, None)
|
|
447
|
+
# 2) 该实例的作业与事件订阅清理(定义级 timer-start 不挂实例,不动)
|
|
448
|
+
for jid in [j.id for j in self._jobs.values()
|
|
449
|
+
if j.process_instance_id == instance_id]:
|
|
450
|
+
self._jobs.pop(jid, None)
|
|
451
|
+
for sid in [s.id for s in self._event_subs.values()
|
|
452
|
+
if s.process_instance_id == instance_id]:
|
|
453
|
+
self._event_subs.pop(sid, None)
|
|
454
|
+
# 3) 实例本体移除
|
|
455
|
+
self._instances.pop(instance_id, None)
|
|
456
|
+
if self._store is not None:
|
|
457
|
+
self._store.delete_proc_inst(instance_id, _now())
|
|
458
|
+
|
|
459
|
+
def get_task(self, task_id: str) -> Task:
|
|
460
|
+
"""按 id 取活跃任务(不存在抛 NotFoundException)。"""
|
|
461
|
+
with self._lock:
|
|
462
|
+
task = self._tasks.get(task_id)
|
|
463
|
+
if task is None:
|
|
464
|
+
raise NotFoundException(f"任务不存在或已完成: {task_id!r}")
|
|
465
|
+
return task
|
|
466
|
+
|
|
467
|
+
def claim_task(self, task_id: str, user_id: str) -> Task:
|
|
468
|
+
"""认领任务:设置 assignee。已认领给他人时报错(Camunda 语义)。"""
|
|
469
|
+
with self._lock:
|
|
470
|
+
task = self._tasks.get(task_id)
|
|
471
|
+
if task is None:
|
|
472
|
+
raise NotFoundException(f"任务不存在或已完成: {task_id!r}")
|
|
473
|
+
if task.assignee is not None and task.assignee != user_id:
|
|
474
|
+
raise InvalidRequestException(
|
|
475
|
+
f"任务 {task_id!r} 已指派给 {task.assignee!r},不能由 {user_id!r} 认领"
|
|
476
|
+
)
|
|
477
|
+
task.assignee = user_id
|
|
478
|
+
self._sync_instance(task.process_instance_id)
|
|
479
|
+
return task
|
|
480
|
+
|
|
481
|
+
def unclaim_task(self, task_id: str) -> Task:
|
|
482
|
+
"""取消认领:清空 assignee(任务回到候选组池)。"""
|
|
483
|
+
with self._lock:
|
|
484
|
+
task = self._tasks.get(task_id)
|
|
485
|
+
if task is None:
|
|
486
|
+
raise NotFoundException(f"任务不存在或已完成: {task_id!r}")
|
|
487
|
+
task.assignee = None
|
|
488
|
+
self._sync_instance(task.process_instance_id)
|
|
489
|
+
return task
|
|
490
|
+
|
|
491
|
+
def set_assignee(self, task_id: str, user_id: Optional[str]) -> Task:
|
|
492
|
+
"""直接指派/清空 assignee(不做「已被他人认领」校验)。"""
|
|
493
|
+
with self._lock:
|
|
494
|
+
task = self._tasks.get(task_id)
|
|
495
|
+
if task is None:
|
|
496
|
+
raise NotFoundException(f"任务不存在或已完成: {task_id!r}")
|
|
497
|
+
task.assignee = user_id
|
|
498
|
+
self._sync_instance(task.process_instance_id)
|
|
499
|
+
return task
|
|
500
|
+
|
|
501
|
+
def set_variable(self, instance_id: str, name: str, value: Any) -> None:
|
|
502
|
+
"""设置实例变量(M6:PUT /process-instance/{id}/variables/{name})。
|
|
503
|
+
|
|
504
|
+
变量为实例级(文档化差异,对齐 M4-2a 起的一贯语义),无作用域隔离。
|
|
505
|
+
"""
|
|
506
|
+
with self._lock:
|
|
507
|
+
pi = self._instances.get(instance_id)
|
|
508
|
+
if pi is None:
|
|
509
|
+
raise NotFoundException(f"流程实例不存在: {instance_id!r}")
|
|
510
|
+
pi.variables[name] = value
|
|
511
|
+
if self._store is not None:
|
|
512
|
+
self._store.save_proc_inst(self._build_snap(pi))
|
|
513
|
+
|
|
514
|
+
def _sync_instance(self, process_instance_id: str) -> None:
|
|
515
|
+
"""任务级变更后同步实例快照(assignee 需落库,否则崩溃恢复丢失)。"""
|
|
516
|
+
if self._store is None:
|
|
517
|
+
return
|
|
518
|
+
pi = self._instances.get(process_instance_id)
|
|
519
|
+
if pi is not None:
|
|
520
|
+
self._store.save_proc_inst(self._build_snap(pi))
|
|
521
|
+
|
|
522
|
+
# ------------------------------------------------------------------
|
|
523
|
+
# M4-2d:消息关联 / 信号广播(RuntimeService correlateMessage 语义)
|
|
524
|
+
# ------------------------------------------------------------------
|
|
525
|
+
def correlate_message(
|
|
526
|
+
self,
|
|
527
|
+
name: str,
|
|
528
|
+
process_instance_id: Optional[str] = None,
|
|
529
|
+
variables: Optional[Dict[str, Any]] = None,
|
|
530
|
+
) -> None:
|
|
531
|
+
"""消息关联:把消息投递给一个等待中的 message 订阅并触发。
|
|
532
|
+
|
|
533
|
+
点对点 1:1(对齐 Camunda message 语义):命中的多个订阅里取注册序最早
|
|
534
|
+
一个(Camunda 用 businessKey/processInstanceId 消歧,本引擎 v1 取最早
|
|
535
|
+
——文档化差异)。process_instance_id 限定后只在实例内匹配。
|
|
536
|
+
|
|
537
|
+
可命中的订阅形态(M4-2d):
|
|
538
|
+
- IntermediateCatchEvent 停等 token(catch);
|
|
539
|
+
- 宿主停等活动上的 message 边界事件(boundary,中断式取消宿主 / 非中断
|
|
540
|
+
spawn 并发线);
|
|
541
|
+
- 事件子流程 message start(宿主 scope 激活期常驻订阅,中断式接管 /
|
|
542
|
+
非中断 spawn,可多次触发)。
|
|
543
|
+
variables 随消息合并进目标实例变量表。无等待订阅 -> NotFoundException。
|
|
544
|
+
"""
|
|
545
|
+
with self._lock:
|
|
546
|
+
sub = self._find_event_subscription("message", name, process_instance_id)
|
|
547
|
+
if sub is None:
|
|
548
|
+
raise NotFoundException(
|
|
549
|
+
f"消息 {name!r} 没有等待中的订阅"
|
|
550
|
+
+ (f"(流程实例 {process_instance_id!r} 内)" if process_instance_id else "")
|
|
551
|
+
)
|
|
552
|
+
pi = self._instances.get(sub.process_instance_id)
|
|
553
|
+
if pi is None or pi.is_completed:
|
|
554
|
+
# 防御:订阅指向已结束实例(清理钩子漏网时惰性回收)
|
|
555
|
+
self._event_subs.pop(sub.id, None)
|
|
556
|
+
raise ProcessInstanceException(
|
|
557
|
+
f"消息 {name!r} 命中的流程实例已结束: {sub.process_instance_id!r}"
|
|
558
|
+
)
|
|
559
|
+
if variables:
|
|
560
|
+
pi.variables.update(variables)
|
|
561
|
+
self._fire_subscription(pi, sub)
|
|
562
|
+
|
|
563
|
+
def throw_signal(
|
|
564
|
+
self,
|
|
565
|
+
name: str,
|
|
566
|
+
variables: Optional[Dict[str, Any]] = None,
|
|
567
|
+
) -> int:
|
|
568
|
+
"""信号广播:把信号投给当前全部等待中的 signal 订阅并触发。
|
|
569
|
+
|
|
570
|
+
广播语义(对齐 Camunda signal):命中所有订阅(跨实例、每实例内多个
|
|
571
|
+
订阅同时触发);无订阅命中则静默无效果(返回 0)。variables 合并到
|
|
572
|
+
每个命中实例的变量表。返回触发的订阅数。
|
|
573
|
+
"""
|
|
574
|
+
with self._lock:
|
|
575
|
+
hit = [
|
|
576
|
+
s
|
|
577
|
+
for s in self._event_subs.values()
|
|
578
|
+
if s.kind == "signal" and s.event_name == name
|
|
579
|
+
]
|
|
580
|
+
for sub in hit:
|
|
581
|
+
pi = self._instances.get(sub.process_instance_id)
|
|
582
|
+
if pi is None or pi.is_completed:
|
|
583
|
+
self._event_subs.pop(sub.id, None) # 惰性回收
|
|
584
|
+
continue
|
|
585
|
+
if variables:
|
|
586
|
+
pi.variables.update(variables)
|
|
587
|
+
self._fire_subscription(pi, sub)
|
|
588
|
+
return len(hit)
|
|
589
|
+
|
|
590
|
+
def _find_event_subscription(
|
|
591
|
+
self,
|
|
592
|
+
kind: str,
|
|
593
|
+
name: str,
|
|
594
|
+
process_instance_id: Optional[str] = None,
|
|
595
|
+
) -> Optional[EventSubscription]:
|
|
596
|
+
"""按(kind, name[, pi])取注册序最早的等待订阅;过期订阅惰性剔除。"""
|
|
597
|
+
for sub in list(self._event_subs.values()):
|
|
598
|
+
if sub.kind != kind or sub.event_name != name:
|
|
599
|
+
continue
|
|
600
|
+
if (
|
|
601
|
+
process_instance_id is not None
|
|
602
|
+
and sub.process_instance_id != process_instance_id
|
|
603
|
+
):
|
|
604
|
+
continue
|
|
605
|
+
pi = self._instances.get(sub.process_instance_id)
|
|
606
|
+
token = pi.executions.get(sub.execution_id) if pi is not None else None
|
|
607
|
+
if (
|
|
608
|
+
pi is None
|
|
609
|
+
or pi.is_completed
|
|
610
|
+
or token is None
|
|
611
|
+
or token.state != ExecutionState.ACTIVE
|
|
612
|
+
):
|
|
613
|
+
self._event_subs.pop(sub.id, None) # 订阅已失效 -> 跳过并回收
|
|
614
|
+
continue
|
|
615
|
+
return sub
|
|
616
|
+
return None
|
|
617
|
+
|
|
618
|
+
def _fire_subscription(self, pi: ProcessInstance, sub: EventSubscription) -> None:
|
|
619
|
+
"""订阅触发统一入口:按挂载形态分派(stale 订阅惰性剔除)。
|
|
620
|
+
|
|
621
|
+
- catch:结算停等 actinst -> token 沿出边推进;
|
|
622
|
+
- boundary:中断式取消宿主(复用 timer 边界取消链)/ 非中断 spawn 并发线;
|
|
623
|
+
- start:事件子流程接管(中断式 interrupt / 非中断直接 spawn)。
|
|
624
|
+
"""
|
|
625
|
+
token = pi.executions.get(sub.execution_id)
|
|
626
|
+
if token is None or token.state != ExecutionState.ACTIVE or pi.is_completed:
|
|
627
|
+
self._event_subs.pop(sub.id, None) # 惰性回收(清理钩子兜底)
|
|
628
|
+
return
|
|
629
|
+
if sub.node_kind == "catch":
|
|
630
|
+
self._fire_event_catch(pi, token, sub)
|
|
631
|
+
return
|
|
632
|
+
if sub.node_kind == "boundary":
|
|
633
|
+
self._fire_event_boundary(pi, token, sub)
|
|
634
|
+
return
|
|
635
|
+
if sub.node_kind == "start":
|
|
636
|
+
self._fire_event_esc_start(pi, token, sub)
|
|
637
|
+
return
|
|
638
|
+
self._event_subs.pop(sub.id, None) # 防御:未知挂载形态
|
|
639
|
+
|
|
640
|
+
def _fire_event_catch(
|
|
641
|
+
self, pi: ProcessInstance, token: Execution, sub: EventSubscription
|
|
642
|
+
) -> None:
|
|
643
|
+
"""message/signal 中间捕获触发:结算停等 -> 沿出边继续。"""
|
|
644
|
+
proc = self._container_of(pi, token)
|
|
645
|
+
node = proc.flow_nodes.get(sub.node_id)
|
|
646
|
+
if (
|
|
647
|
+
not isinstance(node, IntermediateCatchEvent)
|
|
648
|
+
or token.activity_id != node.id
|
|
649
|
+
or token.open_activity is None
|
|
650
|
+
or token.open_activity.end_time is not None
|
|
651
|
+
):
|
|
652
|
+
self._event_subs.pop(sub.id, None) # token 已离开 -> 过期订阅丢弃
|
|
653
|
+
return
|
|
654
|
+
self._event_subs.pop(sub.id, None) # 触发即消费
|
|
655
|
+
self._close_activity(pi, token, node)
|
|
656
|
+
arrivals: List[_Arrival] = []
|
|
657
|
+
self._leave(pi, token, node, arrivals)
|
|
658
|
+
self._pump(pi, arrivals)
|
|
659
|
+
if self._store is not None:
|
|
660
|
+
self._store.save_proc_inst(self._build_snap(pi))
|
|
661
|
+
|
|
662
|
+
def _fire_event_boundary(
|
|
663
|
+
self, pi: ProcessInstance, token: Execution, sub: EventSubscription
|
|
664
|
+
) -> None:
|
|
665
|
+
"""message/signal 边界触发:中断式取消宿主 / 非中断 spawn 并发线。"""
|
|
666
|
+
proc = self._container_of(pi, token)
|
|
667
|
+
boundary = proc.flow_nodes.get(sub.node_id)
|
|
668
|
+
host = (
|
|
669
|
+
proc.flow_nodes.get(boundary.attached_to)
|
|
670
|
+
if isinstance(boundary, BoundaryEvent) and boundary.attached_to
|
|
671
|
+
else None
|
|
672
|
+
)
|
|
673
|
+
if (
|
|
674
|
+
not isinstance(boundary, BoundaryEvent)
|
|
675
|
+
or host is None
|
|
676
|
+
or token.activity_id != host.id
|
|
677
|
+
or token.open_activity is None
|
|
678
|
+
or token.open_activity.end_time is not None
|
|
679
|
+
):
|
|
680
|
+
self._event_subs.pop(sub.id, None) # 宿主已离开 -> 过期订阅丢弃
|
|
681
|
+
return
|
|
682
|
+
if boundary.cancel_activity:
|
|
683
|
+
# 中断式:取消宿主(_cancel_host_activity 撤宿主全部边界订阅含本条),
|
|
684
|
+
# token 改走边界事件出边(与 timer 边界中断路径同构)
|
|
685
|
+
self._cancel_host_activity(pi, token, host)
|
|
686
|
+
self._open_activity(pi, token, boundary)
|
|
687
|
+
self._close_activity(pi, token, boundary)
|
|
688
|
+
arrivals: List[_Arrival] = []
|
|
689
|
+
self._leave(pi, token, boundary, arrivals)
|
|
690
|
+
self._pump(pi, arrivals)
|
|
691
|
+
if self._store is not None:
|
|
692
|
+
self._store.save_proc_inst(self._build_snap(pi))
|
|
693
|
+
else:
|
|
694
|
+
# 非中断式:宿主保留,订阅常驻(可再次触发);spawn 并发线
|
|
695
|
+
self._spawn_non_interrupting_boundary(pi, token, boundary)
|
|
696
|
+
|
|
697
|
+
def _fire_event_esc_start(
|
|
698
|
+
self, pi: ProcessInstance, host: Execution, sub: EventSubscription
|
|
699
|
+
) -> None:
|
|
700
|
+
"""事件子流程 message/signal start 触发(中断式接管 / 非中断 spawn)。"""
|
|
701
|
+
root_proc = self._definitions[pi.process_definition_key]
|
|
702
|
+
# 容器由订阅自身携带:None = 根 Process;否则 = sub_id 对应 sub 的 inner
|
|
703
|
+
# (一致性校验与 timer esc 触发同构:sub 级订阅仅在 host 仍停驻同一 sub)
|
|
704
|
+
if sub.activity_id is None:
|
|
705
|
+
container = root_proc
|
|
706
|
+
else:
|
|
707
|
+
if host.role != "SCOPE" or host.activity_id != sub.activity_id:
|
|
708
|
+
self._event_subs.pop(sub.id, None) # 宿主已离开订阅容器
|
|
709
|
+
return
|
|
710
|
+
outer = self._container_of(pi, host)
|
|
711
|
+
parked = outer.flow_nodes.get(sub.activity_id)
|
|
712
|
+
if not isinstance(parked, SubProcess):
|
|
713
|
+
self._event_subs.pop(sub.id, None)
|
|
714
|
+
return
|
|
715
|
+
container = parked.process
|
|
716
|
+
event_sub: Optional[SubProcess] = None
|
|
717
|
+
start: Optional[StartEvent] = None
|
|
718
|
+
for esc in container.flow_nodes.values():
|
|
719
|
+
if not (isinstance(esc, SubProcess) and esc.triggered_by_event):
|
|
720
|
+
continue
|
|
721
|
+
inner = esc.process
|
|
722
|
+
if inner is None:
|
|
723
|
+
continue
|
|
724
|
+
for st in inner.start_events:
|
|
725
|
+
if st.id == sub.node_id:
|
|
726
|
+
event_sub, start = esc, st
|
|
727
|
+
break
|
|
728
|
+
if event_sub is None or start is None:
|
|
729
|
+
self._event_subs.pop(sub.id, None) # 防御:订阅目标不存在
|
|
730
|
+
return
|
|
731
|
+
if start.is_interrupting:
|
|
732
|
+
# 中断式:触发即消费(随后 _fire_esc_event 整容器订阅一并撤销)
|
|
733
|
+
self._event_subs.pop(sub.id, None)
|
|
734
|
+
# 非中断式:订阅常驻,可再次触发(每次关联/广播 spawn 一个新实例)
|
|
735
|
+
self._fire_esc_event(pi, host, sub.activity_id, event_sub, start)
|
|
736
|
+
|
|
737
|
+
def _fire_esc_event(
|
|
738
|
+
self,
|
|
739
|
+
pi: ProcessInstance,
|
|
740
|
+
host: Execution,
|
|
741
|
+
container_id: Optional[str],
|
|
742
|
+
event_sub: SubProcess,
|
|
743
|
+
start: StartEvent,
|
|
744
|
+
) -> None:
|
|
745
|
+
"""事件子流程 message/signal start 触发主体(中断语义与 timer esc 同构)。
|
|
746
|
+
|
|
747
|
+
中断式:流程级 interrupt 整实例 / sub 级 kill scope 内部;随后撤订阅容器
|
|
748
|
+
全部 esc 订阅(timer job + message/signal sub,宿主被接管同容器订阅失效)。
|
|
749
|
+
非中断式:宿主保留,直接 spawn 事件子流程(message/signal start 订阅常驻
|
|
750
|
+
可再次触发——与 timer 非中断单发的差异,文档化)。
|
|
751
|
+
"""
|
|
752
|
+
if start.is_interrupting:
|
|
753
|
+
if container_id is None:
|
|
754
|
+
self._interrupt_instance(pi) # 流程级:清 root 全部实例级状态
|
|
755
|
+
else:
|
|
756
|
+
self._kill_subprocess_scope(pi, host)
|
|
757
|
+
self._drop_scope_event_jobs(pi, host, container_id)
|
|
758
|
+
self._drop_scope_event_subscriptions(pi, host, container_id)
|
|
759
|
+
arrivals = self._start_event_subprocess(pi, host, event_sub, start)
|
|
760
|
+
self._pump(pi, arrivals)
|
|
761
|
+
if self._store is not None:
|
|
762
|
+
self._store.save_proc_inst(self._build_snap(pi))
|
|
763
|
+
|
|
764
|
+
# ------------------------------------------------------------------
|
|
765
|
+
# TaskService 语义
|
|
766
|
+
# ------------------------------------------------------------------
|
|
767
|
+
def create_task_query(self, process_instance_id: Optional[str] = None) -> List[Task]:
|
|
768
|
+
"""查询任务(M1:无分页,创建序)。"""
|
|
769
|
+
with self._lock:
|
|
770
|
+
tasks = list(self._tasks.values())
|
|
771
|
+
if process_instance_id is not None:
|
|
772
|
+
tasks = [t for t in tasks if t.process_instance_id == process_instance_id]
|
|
773
|
+
return tasks
|
|
774
|
+
|
|
775
|
+
def complete_task(
|
|
776
|
+
self,
|
|
777
|
+
task_id: str,
|
|
778
|
+
variables: Optional[Dict[str, Any]] = None,
|
|
779
|
+
) -> None:
|
|
780
|
+
"""完成任务:合并变量 -> token 从 userTask 离开 -> pump。"""
|
|
781
|
+
with self._lock:
|
|
782
|
+
task = self._tasks.get(task_id)
|
|
783
|
+
if task is None:
|
|
784
|
+
raise NotFoundException(f"任务不存在或已完成: {task_id!r}")
|
|
785
|
+
pi = self._instances[task.process_instance_id]
|
|
786
|
+
if pi.is_completed:
|
|
787
|
+
raise ProcessInstanceException(
|
|
788
|
+
f"流程实例 {pi.id} 已结束,任务 {task_id} 不可再完成"
|
|
789
|
+
)
|
|
790
|
+
token = pi.executions.get(task.execution_id)
|
|
791
|
+
if token is None or token.state != ExecutionState.ACTIVE:
|
|
792
|
+
raise ProcessInstanceException(f"任务 {task_id} 对应的执行已失效")
|
|
793
|
+
|
|
794
|
+
if variables:
|
|
795
|
+
pi.variables.update(variables)
|
|
796
|
+
# 归档到已完成任务(HI_TASKINST 落库),并从活跃任务表移除
|
|
797
|
+
task.end_time = _now()
|
|
798
|
+
pi.completed_tasks.append(task)
|
|
799
|
+
self._tasks.pop(task_id)
|
|
800
|
+
|
|
801
|
+
proc = self._container_of(pi, token)
|
|
802
|
+
node = proc.flow_nodes[task.task_definition_key]
|
|
803
|
+
self._close_activity(pi, token, node)
|
|
804
|
+
self._drop_boundary_jobs(pi, node) # 宿主正常离开:边界 timer 作废
|
|
805
|
+
# 多实例宿主(M4-2c):userTask 是某个 MI 实例的行为载体 ->
|
|
806
|
+
# 走实例完成路径(计数/条件/收束/续跑),不沿普通出边走。
|
|
807
|
+
mi_scope = self._mi_scope_of(pi, token)
|
|
808
|
+
if mi_scope is not None:
|
|
809
|
+
# 实例完成路径会返回续跑/收束推进事件(如顺序 subProcess 宿主启动
|
|
810
|
+
# 下一实例、容器收束沿宿主出边离开),在此统一 pump
|
|
811
|
+
self._pump(pi, self._complete_mi_instance(pi, token, node, mi_scope))
|
|
812
|
+
if self._store is not None:
|
|
813
|
+
self._store.save_proc_inst(self._build_snap(pi))
|
|
814
|
+
return
|
|
815
|
+
arrivals: List[_Arrival] = []
|
|
816
|
+
self._leave(pi, token, node, arrivals)
|
|
817
|
+
self._pump(pi, arrivals)
|
|
818
|
+
if self._store is not None:
|
|
819
|
+
self._store.save_proc_inst(self._build_snap(pi))
|
|
820
|
+
|
|
821
|
+
# ------------------------------------------------------------------
|
|
822
|
+
# 内部推进引擎
|
|
823
|
+
# ------------------------------------------------------------------
|
|
824
|
+
def _pump(self, pi: ProcessInstance, initial: List[_Arrival]) -> None:
|
|
825
|
+
"""事件队列推进:处理 token 到达直至队列空或实例结束。"""
|
|
826
|
+
queue: Deque[_Arrival] = deque(initial)
|
|
827
|
+
while queue and not pi.is_completed:
|
|
828
|
+
token, node = queue.popleft()
|
|
829
|
+
if token.state != ExecutionState.ACTIVE:
|
|
830
|
+
continue # 随兄弟汇聚/取消而失效,丢弃过期事件
|
|
831
|
+
arrivals = self._handle_arrival(pi, token, node)
|
|
832
|
+
queue.extend(arrivals)
|
|
833
|
+
|
|
834
|
+
def _handle_arrival(
|
|
835
|
+
self, pi: ProcessInstance, token: Execution, node: FlowNode
|
|
836
|
+
) -> List[_Arrival]:
|
|
837
|
+
"""token 进入节点:asyncBefore 拆分或按类型分派行为,返回后续到达事件。"""
|
|
838
|
+
token.activity_id = node.id
|
|
839
|
+
|
|
840
|
+
# 多实例宿主(M4-2c):token 首次到达带 multiInstanceLoopCharacteristics
|
|
841
|
+
# 的活动节点 -> 进入 MI 容器语义(parallel spawn / sequential 顺序循环)。
|
|
842
|
+
# token.mi is None 守卫:sequential 续跑/parallel child 由 _start_mi_instance
|
|
843
|
+
# 直接驱动行为,不再经本入口(防重复进入 MI 容器)。
|
|
844
|
+
if node.multi_instance is not None and not token.is_mi_container:
|
|
845
|
+
return self._enter_multi_instance(pi, token, node)
|
|
846
|
+
# asyncBefore:节点行为执行拆成独立 job(async continuation 语义)
|
|
847
|
+
if node.async_before:
|
|
848
|
+
if self._schedule_async_before(pi, token, node):
|
|
849
|
+
return []
|
|
850
|
+
# asyncAfter 支持范围校验(M4-1:serviceTask / exclusiveGateway 行为后拆分;
|
|
851
|
+
# 其余类型文档化差异不支持——userTask/并行网关等 asyncAfter 在 Camunda 有特定
|
|
852
|
+
# 语义,M4-1 明确报错避免静默错位)
|
|
853
|
+
if node.async_after and not isinstance(node, (ServiceTask, ExclusiveGateway)):
|
|
854
|
+
raise InvalidRequestException(
|
|
855
|
+
f"节点 {node.id!r} 声明 camunda:asyncAfter:M4-1 仅支持 serviceTask / "
|
|
856
|
+
f"exclusiveGateway,{type(node).__name__} 的 asyncAfter 不支持(文档化差异)"
|
|
857
|
+
)
|
|
858
|
+
return self._dispatch_node(pi, token, node)
|
|
859
|
+
|
|
860
|
+
def _dispatch_node(
|
|
861
|
+
self, pi: ProcessInstance, token: Execution, node: FlowNode
|
|
862
|
+
) -> List[_Arrival]:
|
|
863
|
+
"""节点行为分派主体(async 作业执行时也直接调本方法,不再重复拆分)。"""
|
|
864
|
+
proc = self._container_of(pi, token)
|
|
865
|
+
arrivals: List[_Arrival] = []
|
|
866
|
+
|
|
867
|
+
if isinstance(node, (StartEvent, EndEvent)):
|
|
868
|
+
self._open_activity(pi, token, node)
|
|
869
|
+
self._close_activity(pi, token, node)
|
|
870
|
+
if isinstance(node, EndEvent):
|
|
871
|
+
if node.error_code:
|
|
872
|
+
# 错误结束事件:token 结束 + 错误冒泡找事件子流程捕获
|
|
873
|
+
return self._throw_error(pi, token, node)
|
|
874
|
+
if node.message_name is not None or node.signal_name is not None:
|
|
875
|
+
# M4-2d 消息/信号结束:token 结束同时实例内投递;若自身广播
|
|
876
|
+
# 触发的中断式订阅接管了实例(token 被杀)则不再收束
|
|
877
|
+
self._throw_event_in_instance(pi, token, node)
|
|
878
|
+
if token.state != ExecutionState.ACTIVE or pi.is_completed:
|
|
879
|
+
return []
|
|
880
|
+
arrivals.extend(self._end_token(pi, token))
|
|
881
|
+
else:
|
|
882
|
+
self._leave(pi, token, node, arrivals)
|
|
883
|
+
return arrivals
|
|
884
|
+
|
|
885
|
+
if isinstance(node, SubProcess):
|
|
886
|
+
return self._enter_subprocess(pi, token, node)
|
|
887
|
+
|
|
888
|
+
if isinstance(node, IntermediateCatchEvent):
|
|
889
|
+
return self._enter_event_catch(pi, token, node)
|
|
890
|
+
|
|
891
|
+
if isinstance(node, IntermediateThrowEvent):
|
|
892
|
+
# M4-2d:中间抛出事件(无等待窗口):结算 actinst -> 实例内投递
|
|
893
|
+
# message/signal -> token 沿出边继续(无出边则收束)。若投递触发
|
|
894
|
+
# 的中断式订阅接管了实例(token 被杀)则不再继续
|
|
895
|
+
self._open_activity(pi, token, node)
|
|
896
|
+
self._close_activity(pi, token, node)
|
|
897
|
+
if node.message_name is None and node.signal_name is None:
|
|
898
|
+
raise InvalidRequestException(
|
|
899
|
+
f"中间抛出事件 {node.id!r} 未实现"
|
|
900
|
+
"(M4-2d 仅支持 message/signal throw)"
|
|
901
|
+
)
|
|
902
|
+
self._throw_event_in_instance(pi, token, node)
|
|
903
|
+
if token.state != ExecutionState.ACTIVE or pi.is_completed:
|
|
904
|
+
return []
|
|
905
|
+
self._leave(pi, token, node, arrivals)
|
|
906
|
+
return arrivals
|
|
907
|
+
|
|
908
|
+
if isinstance(node, UserTask):
|
|
909
|
+
return self._enter_user_task_wait(pi, token, node)
|
|
910
|
+
|
|
911
|
+
if isinstance(node, ServiceTask):
|
|
912
|
+
self._open_activity(pi, token, node)
|
|
913
|
+
self._run_delegate(pi, token, node)
|
|
914
|
+
self._close_activity(pi, token, node)
|
|
915
|
+
if node.async_after:
|
|
916
|
+
# 行为已完成(actinst 结算):把「离开推进」拆成独立 async-after job
|
|
917
|
+
self._schedule_async_after(pi, token, node)
|
|
918
|
+
return arrivals
|
|
919
|
+
self._leave(pi, token, node, arrivals)
|
|
920
|
+
return arrivals
|
|
921
|
+
|
|
922
|
+
if isinstance(node, BusinessRuleTask):
|
|
923
|
+
# M5:同步求值 DMN 决策(无等待窗口)-> 结果写入 result_variable
|
|
924
|
+
self._open_activity(pi, token, node)
|
|
925
|
+
result = self._dmn.evaluate_decision(node.decision_ref, pi.variables)
|
|
926
|
+
pi.variables[node.result_variable] = result
|
|
927
|
+
self._close_activity(pi, token, node)
|
|
928
|
+
if node.async_after:
|
|
929
|
+
self._schedule_async_after(pi, token, node)
|
|
930
|
+
return arrivals
|
|
931
|
+
self._leave(pi, token, node, arrivals)
|
|
932
|
+
return arrivals
|
|
933
|
+
|
|
934
|
+
if isinstance(node, ExclusiveGateway):
|
|
935
|
+
self._open_activity(pi, token, node)
|
|
936
|
+
self._close_activity(pi, token, node)
|
|
937
|
+
if node.async_after:
|
|
938
|
+
# 网关无副作用:选路推迟到 async-after job(到期重新求值条件)
|
|
939
|
+
self._schedule_async_after(pi, token, node)
|
|
940
|
+
return arrivals
|
|
941
|
+
chosen = select_exclusive_gateway_flow(
|
|
942
|
+
node, self._outgoing(proc, node), pi.variables
|
|
943
|
+
)
|
|
944
|
+
self._take(pi, token, chosen, arrivals)
|
|
945
|
+
return arrivals
|
|
946
|
+
|
|
947
|
+
if isinstance(node, ParallelGateway):
|
|
948
|
+
return self._handle_parallel_gateway(pi, token, node)
|
|
949
|
+
|
|
950
|
+
raise InvalidRequestException(
|
|
951
|
+
f"不支持的节点类型: {type(node).__name__} (id={node.id})"
|
|
952
|
+
)
|
|
953
|
+
|
|
954
|
+
# ------------------------------------------------------------------
|
|
955
|
+
# M4-2a:embedded SubProcess 进入/收束(SCOPE 容器语义)
|
|
956
|
+
# ------------------------------------------------------------------
|
|
957
|
+
def _container_of(self, pi: ProcessInstance, e: Execution) -> Process:
|
|
958
|
+
"""execution 当前所在容器(内嵌子流程展开时随树推导,无需额外存储)。
|
|
959
|
+
|
|
960
|
+
沿父链向上找第一个「停驻在 SubProcess 上的 SCOPE 祖先」——该祖先代表
|
|
961
|
+
子流程执行体,取其 inner Process 即当前容器;无则根 Process。跨容器
|
|
962
|
+
节点 id 不串扰(不同容器可有同名节点,归属只按树位置解析)。
|
|
963
|
+
"""
|
|
964
|
+
cur: Optional[Execution] = e
|
|
965
|
+
while cur.parent_id is not None:
|
|
966
|
+
parent = pi.executions.get(cur.parent_id)
|
|
967
|
+
if parent is None:
|
|
968
|
+
break
|
|
969
|
+
if parent.role == "SCOPE" and parent.activity_id:
|
|
970
|
+
pnode = (
|
|
971
|
+
self._container_of(pi, parent)
|
|
972
|
+
.flow_nodes.get(parent.activity_id)
|
|
973
|
+
)
|
|
974
|
+
if isinstance(pnode, SubProcess):
|
|
975
|
+
# M4-2c3:仅「并行 MI 容器」停驻在宿主 subProcess 节点上但
|
|
976
|
+
# 不进子流程(进入的是其 child 实例)——它不是执行体,跳过
|
|
977
|
+
# 继续向外。其余 SCOPE@sub 都是该 sub 的执行体,取其 inner:
|
|
978
|
+
# 常规 embedded scope / 顺序 MI 容器(token 兼实例载体)/
|
|
979
|
+
# 并行实例载体(mi={"index"},本身已进 sub 跑内部流转)。
|
|
980
|
+
if parent.is_mi_container and not parent.mi["sequential"]:
|
|
981
|
+
cur = parent
|
|
982
|
+
continue
|
|
983
|
+
return pnode.process
|
|
984
|
+
cur = parent
|
|
985
|
+
return self._definitions[pi.process_definition_key]
|
|
986
|
+
|
|
987
|
+
def _enter_subprocess(
|
|
988
|
+
self, pi: ProcessInstance, token: Execution, sub: SubProcess
|
|
989
|
+
) -> List[_Arrival]:
|
|
990
|
+
"""进入内嵌子流程:token 停驻为 SCOPE,spawn 内部子 token 从 startEvent 推进。
|
|
991
|
+
|
|
992
|
+
- subProcess 活动实例跨整段内部执行 open(exit 时结算),对齐 Camunda
|
|
993
|
+
HI_ACTINST 对 subProcess 的覆盖区间。
|
|
994
|
+
- 变量作用域沿用实例级(文档化差异,无子作用域遮蔽)。
|
|
995
|
+
- 边界 timer 注册:subProcess 是合法宿主,等待窗口 = 整段内部执行期
|
|
996
|
+
(M4-2a3;非中断式 cancelActivity=false 仍拒绝)。
|
|
997
|
+
- 事件子流程(triggeredByEvent)运行语义 M4-2b 落地,先明确报错。
|
|
998
|
+
"""
|
|
999
|
+
if sub.triggered_by_event:
|
|
1000
|
+
raise InvalidRequestException(
|
|
1001
|
+
f"subProcess {sub.id!r} 是事件子流程(triggeredByEvent):"
|
|
1002
|
+
f"M4-2b 实现,当前不支持"
|
|
1003
|
+
)
|
|
1004
|
+
inner = sub.process
|
|
1005
|
+
if inner is None or not inner.start_events:
|
|
1006
|
+
raise ProcessInstanceException(
|
|
1007
|
+
f"subProcess {sub.id!r} 内部没有可启动的 startEvent"
|
|
1008
|
+
)
|
|
1009
|
+
start = inner.start_events[0]
|
|
1010
|
+
if start.timer is not None:
|
|
1011
|
+
raise InvalidRequestException(
|
|
1012
|
+
f"subProcess {sub.id!r} 内部 startEvent {start.id!r} 带 timer:"
|
|
1013
|
+
f"内嵌子流程不支持定时启动(文档化差异)"
|
|
1014
|
+
)
|
|
1015
|
+
if start.error_code is not None or start.message_name is not None or start.signal_name is not None:
|
|
1016
|
+
raise InvalidRequestException(
|
|
1017
|
+
f"subProcess {sub.id!r} 内部 startEvent {start.id!r} 是事件 start"
|
|
1018
|
+
"(error/message/signal):事件启动只属于事件子流程(文档化差异)"
|
|
1019
|
+
)
|
|
1020
|
+
self._open_activity(pi, token, sub)
|
|
1021
|
+
token.role = "SCOPE"
|
|
1022
|
+
# spawn 内部子 token:从 subProcess 内部 startEvent 开始推进
|
|
1023
|
+
child = Execution(
|
|
1024
|
+
id=self._idgen.next_id(),
|
|
1025
|
+
process_instance_id=pi.id,
|
|
1026
|
+
parent_id=token.id,
|
|
1027
|
+
)
|
|
1028
|
+
pi.executions[child.id] = child
|
|
1029
|
+
token.children.append(child)
|
|
1030
|
+
# 进入等待窗口:注册挂在 subProcess 上的边界 timer 作业
|
|
1031
|
+
self._register_boundary_jobs(pi, token, sub)
|
|
1032
|
+
# M4-2b3:subProcess scope 激活 -> 订阅该 sub 容器内事件子流程的 timer start
|
|
1033
|
+
self._register_event_subprocess_timers(pi, token, sub.process, sub.id)
|
|
1034
|
+
# M4-2d:该 sub 容器内 message/signal esc start 常驻订阅
|
|
1035
|
+
self._register_event_subprocess_subscriptions(pi, token, sub.process, sub.id)
|
|
1036
|
+
return [(child, start)]
|
|
1037
|
+
|
|
1038
|
+
# ------------------------------------------------------------------
|
|
1039
|
+
# M4-2b:事件子流程(triggeredByEvent)与错误传播
|
|
1040
|
+
# ------------------------------------------------------------------
|
|
1041
|
+
def _throw_error(
|
|
1042
|
+
self, pi: ProcessInstance, token: Execution, node: EndEvent
|
|
1043
|
+
) -> List[_Arrival]:
|
|
1044
|
+
"""error endEvent 抛出:token 结束 + 错误沿宿主链冒泡找事件子流程捕获。
|
|
1045
|
+
|
|
1046
|
+
命中 -> 中断宿主 scope 其它执行、事件子流程接管(其结束 = 宿主 scope
|
|
1047
|
+
结束:流程级 -> 实例完成;subProcess 级 -> 收束复活沿出边继续——错误由
|
|
1048
|
+
subProcess 自己消化)。未命中 -> 等同 none end(仅当前路径结束),对齐
|
|
1049
|
+
Camunda error end event 默认语义(日志 warning 留痕)。
|
|
1050
|
+
"""
|
|
1051
|
+
code = node.error_code
|
|
1052
|
+
hit = self._find_error_catcher(pi, token, code)
|
|
1053
|
+
if hit is None:
|
|
1054
|
+
logger.warning(
|
|
1055
|
+
"流程实例 %s: error endEvent %s 抛出错误 %r 无事件子流程捕获,等同 none end",
|
|
1056
|
+
pi.id, node.id, code,
|
|
1057
|
+
)
|
|
1058
|
+
return self._end_token(pi, token)
|
|
1059
|
+
host, container, event_sub, start = hit
|
|
1060
|
+
if token.is_root or pi.root_execution is token:
|
|
1061
|
+
# 根执行到达 error end:实例整体被事件子流程接管。root 不结束——它
|
|
1062
|
+
# 转为事件子流程宿主载体(活动/子树清空由 _interrupt_instance 完成,
|
|
1063
|
+
# 事件子流程收束后 collapse 收尾 -> 实例完成)
|
|
1064
|
+
self._interrupt_instance(pi)
|
|
1065
|
+
else:
|
|
1066
|
+
# 子树内抛错:错误路径结束摘树,再中断宿主 scope 的其它执行
|
|
1067
|
+
token.state = ExecutionState.ENDED
|
|
1068
|
+
self._detach_from_parent(pi, token)
|
|
1069
|
+
if container is self._definitions[pi.process_definition_key]:
|
|
1070
|
+
self._interrupt_instance(pi)
|
|
1071
|
+
else:
|
|
1072
|
+
# subProcess(或嵌套事件子流程)级捕获:中断该容器 scope 内部,
|
|
1073
|
+
# 宿主本体保留(事件子流程走完后由收束链复活/上移)
|
|
1074
|
+
self._kill_subprocess_scope(pi, host)
|
|
1075
|
+
# 宿主被事件子流程接管:其容器内 timer 事件子流程订阅一并失效
|
|
1076
|
+
# (只撤被接管 sub 容器的订阅——host 若为 root 兼任,流程级订阅保留)
|
|
1077
|
+
self._drop_scope_event_jobs(pi, host, host.activity_id)
|
|
1078
|
+
# M4-2d:同容器 message/signal esc 订阅一并失效
|
|
1079
|
+
self._drop_scope_event_subscriptions(pi, host, host.activity_id)
|
|
1080
|
+
return self._start_event_subprocess(pi, host, event_sub, start)
|
|
1081
|
+
|
|
1082
|
+
def _throw_event_in_instance(
|
|
1083
|
+
self, pi: ProcessInstance, token: Execution, node: FlowNode
|
|
1084
|
+
) -> None:
|
|
1085
|
+
"""实例内 message/signal throw(M4-2d:IntermediateThrowEvent / EndEvent)。
|
|
1086
|
+
|
|
1087
|
+
- message:1:1 就近关联——本实例内匹配订阅取注册序最早(文档化差异:
|
|
1088
|
+
Camunda 沿 scope 链向外找最近命中;本引擎统一按注册序)。未命中 ->
|
|
1089
|
+
静默丢弃(对齐 Camunda throw message 无等待订阅即丢失,等同 none end)。
|
|
1090
|
+
- signal:本实例内广播全部匹配订阅(文档化差异:跨实例广播由公共 API
|
|
1091
|
+
throw_signal 提供,throw 事件本身只作用于本实例)。
|
|
1092
|
+
触发可能接管实例(中断式订阅杀死本 token)——调用方检查 token 存活性。
|
|
1093
|
+
"""
|
|
1094
|
+
kind = "message" if node.message_name is not None else "signal"
|
|
1095
|
+
name = node.message_name or node.signal_name
|
|
1096
|
+
if kind == "message":
|
|
1097
|
+
sub = self._find_event_subscription("message", name, pi.id)
|
|
1098
|
+
if sub is None:
|
|
1099
|
+
logger.warning(
|
|
1100
|
+
"流程实例 %s: throw message %r 无等待订阅,消息丢弃(token 继续流转)",
|
|
1101
|
+
pi.id, name,
|
|
1102
|
+
)
|
|
1103
|
+
return
|
|
1104
|
+
self._fire_subscription(pi, sub)
|
|
1105
|
+
return
|
|
1106
|
+
for sub in [
|
|
1107
|
+
s
|
|
1108
|
+
for s in self._event_subs.values()
|
|
1109
|
+
if s.kind == "signal"
|
|
1110
|
+
and s.event_name == name
|
|
1111
|
+
and s.process_instance_id == pi.id
|
|
1112
|
+
]:
|
|
1113
|
+
self._fire_subscription(pi, sub)
|
|
1114
|
+
|
|
1115
|
+
def _find_error_catcher(
|
|
1116
|
+
self, pi: ProcessInstance, e: Execution, code: str
|
|
1117
|
+
) -> Optional[Tuple[Execution, "Process", SubProcess, StartEvent]]:
|
|
1118
|
+
"""沿宿主 scope 链(内到外)找能捕获错误 code 的事件子流程。
|
|
1119
|
+
|
|
1120
|
+
返回 (host_execution, container, event_sub, start):
|
|
1121
|
+
- host_execution:root 或停驻 SubProcess 的 SCOPE(事件子流程的宿主)
|
|
1122
|
+
- container:命中声明所在的容器 Process(根 Process = 流程级,中断整个
|
|
1123
|
+
实例;某 subProcess 的 inner = subProcess 级,中断该 scope 内部)
|
|
1124
|
+
- event_sub / start:命中的事件子流程与其 error start
|
|
1125
|
+
无命中返回 None(调用方按 none end 语义处理)。
|
|
1126
|
+
"""
|
|
1127
|
+
root_proc = self._definitions[pi.process_definition_key]
|
|
1128
|
+
# 宿主链:从 e 所在容器逐层向外到根容器(先查内层声明,再冒到外层)
|
|
1129
|
+
chain: List[Tuple[Execution, Process]] = []
|
|
1130
|
+
cur = e
|
|
1131
|
+
while cur is not None and not (cur.is_root or pi.root_execution is cur):
|
|
1132
|
+
parent = pi.executions.get(cur.parent_id) if cur.parent_id else None
|
|
1133
|
+
if parent is None:
|
|
1134
|
+
break
|
|
1135
|
+
if parent.role == "SCOPE" and parent.activity_id:
|
|
1136
|
+
pnode = (
|
|
1137
|
+
self._container_of(pi, parent).flow_nodes.get(parent.activity_id)
|
|
1138
|
+
)
|
|
1139
|
+
if isinstance(pnode, SubProcess):
|
|
1140
|
+
chain.append((parent, pnode.process))
|
|
1141
|
+
cur = parent
|
|
1142
|
+
chain.append((pi.root_execution, root_proc))
|
|
1143
|
+
for host, container in chain:
|
|
1144
|
+
for sub in container.flow_nodes.values():
|
|
1145
|
+
if not (isinstance(sub, SubProcess) and sub.triggered_by_event):
|
|
1146
|
+
continue
|
|
1147
|
+
inner = sub.process
|
|
1148
|
+
if inner is None:
|
|
1149
|
+
continue
|
|
1150
|
+
for st in inner.start_events:
|
|
1151
|
+
if st.error_code == code and st.is_interrupting:
|
|
1152
|
+
return host, container, sub, st
|
|
1153
|
+
return None
|
|
1154
|
+
|
|
1155
|
+
def _start_event_subprocess(
|
|
1156
|
+
self,
|
|
1157
|
+
pi: ProcessInstance,
|
|
1158
|
+
host: Execution,
|
|
1159
|
+
event_sub: SubProcess,
|
|
1160
|
+
start: StartEvent,
|
|
1161
|
+
) -> List[_Arrival]:
|
|
1162
|
+
"""在宿主 scope 下启动事件子流程实例(中断由调用方先行完成)。
|
|
1163
|
+
|
|
1164
|
+
- 事件子流程 scope:role=SCOPE、activity_id=事件子流程 id、挂 host 下
|
|
1165
|
+
——与 embedded SubProcess 共用容器推导/收束路径(零表改动)。事件子
|
|
1166
|
+
流程 actinst 跨整段执行 open,收束时由 collapse 的 SubProcess 分支结算。
|
|
1167
|
+
- 内部子 token 从匹配的 startEvent 推进(start 的 event 槽仅用于触发,
|
|
1168
|
+
运行语义 = 普通 startEvent 沿出边走)。
|
|
1169
|
+
- message/signal start(M4-2d):与 timer/error start 同型——订阅触发
|
|
1170
|
+
由调用方(correlate_message / throw_signal)完成中断语义后进入。
|
|
1171
|
+
"""
|
|
1172
|
+
inner = event_sub.process
|
|
1173
|
+
if inner is None:
|
|
1174
|
+
raise ProcessInstanceException(
|
|
1175
|
+
f"事件子流程 {event_sub.id!r} 内部容器缺失"
|
|
1176
|
+
)
|
|
1177
|
+
# message/signal start(M4-2d):仅经由 _fire_esc_event 进入(correlate/
|
|
1178
|
+
# throw_signal 触发路径已完成中断语义);timer/error 路径按事件槽匹配,
|
|
1179
|
+
# 不会命中 message/signal start,无需额外守卫。
|
|
1180
|
+
scope = Execution(
|
|
1181
|
+
id=self._idgen.next_id(),
|
|
1182
|
+
process_instance_id=pi.id,
|
|
1183
|
+
parent_id=host.id,
|
|
1184
|
+
role="SCOPE",
|
|
1185
|
+
activity_id=event_sub.id,
|
|
1186
|
+
)
|
|
1187
|
+
pi.executions[scope.id] = scope
|
|
1188
|
+
host.children.append(scope)
|
|
1189
|
+
child = Execution(
|
|
1190
|
+
id=self._idgen.next_id(),
|
|
1191
|
+
process_instance_id=pi.id,
|
|
1192
|
+
parent_id=scope.id,
|
|
1193
|
+
)
|
|
1194
|
+
pi.executions[child.id] = child
|
|
1195
|
+
scope.children.append(child)
|
|
1196
|
+
self._open_activity(pi, scope, event_sub)
|
|
1197
|
+
# M4-2b3:事件子流程 scope 自身激活 -> 订阅其内部嵌套事件子流程的 timer
|
|
1198
|
+
# start(宿主容器 = 本事件子流程的 inner)
|
|
1199
|
+
self._register_event_subprocess_timers(pi, scope, inner, event_sub.id)
|
|
1200
|
+
# M4-2d:事件子流程 scope 自身激活 -> 订阅其内部嵌套事件子流程的
|
|
1201
|
+
# message/signal start(宿主容器 = 本事件子流程的 inner)
|
|
1202
|
+
self._register_event_subprocess_subscriptions(pi, scope, inner, event_sub.id)
|
|
1203
|
+
return [(child, start)]
|
|
1204
|
+
|
|
1205
|
+
def _interrupt_instance(self, pi: ProcessInstance) -> None:
|
|
1206
|
+
"""流程级中断:结束实例内全部执行,root 保留为事件子流程宿主载体。
|
|
1207
|
+
|
|
1208
|
+
结算 root 自身活动/任务/作业,杀全部子树(含 join 登记摘除、任务归档),
|
|
1209
|
+
清空 root.activity_id(事件子流程 scope 挂其下;收束后 collapse 收尾)。
|
|
1210
|
+
"""
|
|
1211
|
+
now = _now()
|
|
1212
|
+
root = pi.root_execution
|
|
1213
|
+
if root is None:
|
|
1214
|
+
return
|
|
1215
|
+
if root.open_activity is not None and root.open_activity.end_time is None:
|
|
1216
|
+
root.open_activity.end_time = now
|
|
1217
|
+
root.open_activity = None
|
|
1218
|
+
for t in [
|
|
1219
|
+
t
|
|
1220
|
+
for t in self._tasks.values()
|
|
1221
|
+
if t.process_instance_id == pi.id and t.execution_id == root.id
|
|
1222
|
+
]:
|
|
1223
|
+
self._tasks.pop(t.id)
|
|
1224
|
+
t.end_time = now
|
|
1225
|
+
pi.completed_tasks.append(t)
|
|
1226
|
+
for j in [
|
|
1227
|
+
j
|
|
1228
|
+
for j in self._jobs.values()
|
|
1229
|
+
if j.process_instance_id == pi.id and j.execution_id == root.id
|
|
1230
|
+
]:
|
|
1231
|
+
self._jobs.pop(j.id)
|
|
1232
|
+
self._kill_subprocess_scope(pi, root) # 子树全杀(任务归档/作业删除/join 摘除)
|
|
1233
|
+
# M4-2d:root 自身承载的订阅(流程级/message/signal esc、边界)一并作废
|
|
1234
|
+
# ——中断式触发方已撤容器订阅,此处兜底剩余(对齐 timer 全撤语义)
|
|
1235
|
+
self._drop_subscriptions_for_execution(pi, root.id)
|
|
1236
|
+
pi.join_arrivals.clear() # 兜底清残留登记(kill 已逐棵摘除)
|
|
1237
|
+
root.activity_id = None
|
|
1238
|
+
root.role = "TOKEN" # 复位:root 作为实例级事件子流程的宿主载体
|
|
1239
|
+
|
|
1240
|
+
def _register_event_subprocess_timers(
|
|
1241
|
+
self,
|
|
1242
|
+
pi: ProcessInstance,
|
|
1243
|
+
host_scope: Execution,
|
|
1244
|
+
container: "Process",
|
|
1245
|
+
sub_id: Optional[str],
|
|
1246
|
+
) -> None:
|
|
1247
|
+
"""宿主 scope 激活订阅:容器内事件子流程的 timer start 注册实例级作业。
|
|
1248
|
+
|
|
1249
|
+
container/sub_id 由调用点显式给出(流程实例启动 -> 根 Process/None;
|
|
1250
|
+
进入 embedded subProcess -> 该 sub 的 inner/sub.id),不反推——root 兼任
|
|
1251
|
+
sub 容器时(root 停驻 sub)两处订阅必须落在各自容器上、互不混淆。
|
|
1252
|
+
|
|
1253
|
+
订阅生命周期对齐 Camunda 文档:scope(流程实例或 subProcess)创建即订阅、
|
|
1254
|
+
scope 结束或触发即撤销(_drop_scope_event_jobs)。每次激活单发:timer
|
|
1255
|
+
事件子流程不支持 cycle(文档化差异),非中断式 timer 同样只触发一次。
|
|
1256
|
+
"""
|
|
1257
|
+
for sub in container.flow_nodes.values():
|
|
1258
|
+
if not (isinstance(sub, SubProcess) and sub.triggered_by_event):
|
|
1259
|
+
continue
|
|
1260
|
+
inner = sub.process
|
|
1261
|
+
if inner is None:
|
|
1262
|
+
continue
|
|
1263
|
+
for st in inner.start_events:
|
|
1264
|
+
if st.timer is None:
|
|
1265
|
+
continue
|
|
1266
|
+
# 幂等:重复激活(如顺序 MI 宿主续跑再进同一 sub)不重复注册
|
|
1267
|
+
dup = any(
|
|
1268
|
+
j.job_type == "timer-event-start"
|
|
1269
|
+
and j.process_instance_id == pi.id
|
|
1270
|
+
and j.execution_id == host_scope.id
|
|
1271
|
+
and j.activity_id == sub_id
|
|
1272
|
+
and j.node_id == st.id
|
|
1273
|
+
for j in self._jobs.values()
|
|
1274
|
+
)
|
|
1275
|
+
if dup:
|
|
1276
|
+
continue
|
|
1277
|
+
self._register_timer_event_start_job(pi, host_scope, sub, st, sub_id)
|
|
1278
|
+
|
|
1279
|
+
def _register_timer_event_start_job(
|
|
1280
|
+
self,
|
|
1281
|
+
pi: ProcessInstance,
|
|
1282
|
+
host_scope: Execution,
|
|
1283
|
+
event_sub: SubProcess,
|
|
1284
|
+
start: StartEvent,
|
|
1285
|
+
sub_id: Optional[str],
|
|
1286
|
+
) -> None:
|
|
1287
|
+
"""为事件子流程的 timer start 注册一条 timer-event-start 作业(订阅)。"""
|
|
1288
|
+
timer = start.timer
|
|
1289
|
+
if timer.kind == "cycle":
|
|
1290
|
+
raise InvalidRequestException(
|
|
1291
|
+
f"事件子流程 {event_sub.id!r} 的 timer start {start.id!r} 不支持 "
|
|
1292
|
+
f"timeCycle(订阅单发,文档化差异)"
|
|
1293
|
+
)
|
|
1294
|
+
now = _now()
|
|
1295
|
+
if timer.kind == "duration":
|
|
1296
|
+
duedate = format_iso(
|
|
1297
|
+
parse_iso(now) + timedelta(seconds=timer.delay_seconds or 0)
|
|
1298
|
+
)
|
|
1299
|
+
else: # date:绝对时间点
|
|
1300
|
+
duedate = parse_trigger_date(timer.value)
|
|
1301
|
+
job = Job(
|
|
1302
|
+
id=self._idgen.next_id(),
|
|
1303
|
+
job_type="timer-event-start",
|
|
1304
|
+
duedate=duedate,
|
|
1305
|
+
created=now,
|
|
1306
|
+
process_instance_id=pi.id,
|
|
1307
|
+
execution_id=host_scope.id, # 宿主 scope;容器/事件子流程到期反查
|
|
1308
|
+
node_id=start.id, # 事件子流程 startEvent id
|
|
1309
|
+
activity_id=sub_id, # 订阅容器 subProcess id(None = 流程级)
|
|
1310
|
+
)
|
|
1311
|
+
self._jobs[job.id] = job
|
|
1312
|
+
|
|
1313
|
+
def _drop_scope_event_jobs(
|
|
1314
|
+
self,
|
|
1315
|
+
pi: ProcessInstance,
|
|
1316
|
+
host_scope: Execution,
|
|
1317
|
+
sub_id: Optional[str] = None,
|
|
1318
|
+
node_id: Optional[str] = None,
|
|
1319
|
+
) -> None:
|
|
1320
|
+
"""宿主 scope 事件子流程订阅撤销:按 (execution, 容器[, start]) 精确删除。
|
|
1321
|
+
|
|
1322
|
+
sub_id=None 只撤流程级(根 Process 容器)订阅;传 sub_id 只撤该 subProcess
|
|
1323
|
+
容器上的订阅——root 兼任 sub 容器时两套订阅可独立撤销,互不误伤。
|
|
1324
|
+
node_id 给定则只撤该 timer start 的订阅(非中断式单发消费)。
|
|
1325
|
+
"""
|
|
1326
|
+
stale = [
|
|
1327
|
+
jid
|
|
1328
|
+
for jid, j in self._jobs.items()
|
|
1329
|
+
if j.process_instance_id == pi.id
|
|
1330
|
+
and j.job_type == "timer-event-start"
|
|
1331
|
+
and j.execution_id == host_scope.id
|
|
1332
|
+
and j.activity_id == sub_id
|
|
1333
|
+
and (node_id is None or j.node_id == node_id)
|
|
1334
|
+
]
|
|
1335
|
+
for jid in stale:
|
|
1336
|
+
self._jobs.pop(jid, None)
|
|
1337
|
+
|
|
1338
|
+
def _register_event_subprocess_subscriptions(
|
|
1339
|
+
self,
|
|
1340
|
+
pi: ProcessInstance,
|
|
1341
|
+
host_scope: Execution,
|
|
1342
|
+
container: "Process",
|
|
1343
|
+
sub_id: Optional[str],
|
|
1344
|
+
) -> None:
|
|
1345
|
+
"""宿主 scope 激活订阅:容器内事件子流程的 message/signal start 注册订阅。
|
|
1346
|
+
|
|
1347
|
+
与 timer start(实例级 job)平行;message/signal 是无限期等待 + 外部
|
|
1348
|
+
关联/广播触发 -> 常驻订阅表(_event_subs),宿主 scope 生命周期内有效:
|
|
1349
|
+
- 中断式 start:触发时撤容器全部 esc 订阅(宿主被接管);
|
|
1350
|
+
- 非中断式 start:常驻可多次触发(每次触发 spawn 一个新实例)。
|
|
1351
|
+
幂等:同(宿主, 容器, start)重复激活(如顺序 MI 续跑再进同一 sub)
|
|
1352
|
+
不重复注册。container/sub_id 由调用点显式给出(同 timer 注册约定)。
|
|
1353
|
+
"""
|
|
1354
|
+
for sub in container.flow_nodes.values():
|
|
1355
|
+
if not (isinstance(sub, SubProcess) and sub.triggered_by_event):
|
|
1356
|
+
continue
|
|
1357
|
+
inner = sub.process
|
|
1358
|
+
if inner is None:
|
|
1359
|
+
continue
|
|
1360
|
+
for st in inner.start_events:
|
|
1361
|
+
if st.message_name is None and st.signal_name is None:
|
|
1362
|
+
continue
|
|
1363
|
+
kind = "message" if st.message_name is not None else "signal"
|
|
1364
|
+
name = st.message_name or st.signal_name
|
|
1365
|
+
dup = any(
|
|
1366
|
+
s.kind == kind
|
|
1367
|
+
and s.event_name == name
|
|
1368
|
+
and s.execution_id == host_scope.id
|
|
1369
|
+
and s.activity_id == sub_id
|
|
1370
|
+
and s.node_id == st.id
|
|
1371
|
+
for s in self._event_subs.values()
|
|
1372
|
+
)
|
|
1373
|
+
if dup:
|
|
1374
|
+
continue # 幂等:重复激活不重复注册
|
|
1375
|
+
s = EventSubscription(
|
|
1376
|
+
id=self._idgen.next_id(),
|
|
1377
|
+
kind=kind,
|
|
1378
|
+
event_name=name,
|
|
1379
|
+
process_instance_id=pi.id,
|
|
1380
|
+
execution_id=host_scope.id,
|
|
1381
|
+
activity_id=sub_id,
|
|
1382
|
+
node_id=st.id,
|
|
1383
|
+
node_kind="start",
|
|
1384
|
+
is_interrupting=st.is_interrupting,
|
|
1385
|
+
created=_now(),
|
|
1386
|
+
)
|
|
1387
|
+
self._event_subs[s.id] = s
|
|
1388
|
+
|
|
1389
|
+
def _drop_scope_event_subscriptions(
|
|
1390
|
+
self,
|
|
1391
|
+
pi: ProcessInstance,
|
|
1392
|
+
host_scope: Execution,
|
|
1393
|
+
sub_id: Optional[str] = None,
|
|
1394
|
+
node_id: Optional[str] = None,
|
|
1395
|
+
) -> None:
|
|
1396
|
+
"""宿主 scope 事件子流程消息/信号订阅撤销(按 execution + 容器精确删)。
|
|
1397
|
+
|
|
1398
|
+
sub_id=None 只撤流程级订阅;传 sub_id 只撤该 subProcess 容器上的订阅。
|
|
1399
|
+
node_id 给定则只撤该 start 的订阅(触发消费用)。
|
|
1400
|
+
"""
|
|
1401
|
+
stale = [
|
|
1402
|
+
sid
|
|
1403
|
+
for sid, s in self._event_subs.items()
|
|
1404
|
+
if s.process_instance_id == pi.id
|
|
1405
|
+
and s.node_kind == "start"
|
|
1406
|
+
and s.execution_id == host_scope.id
|
|
1407
|
+
and s.activity_id == sub_id
|
|
1408
|
+
and (node_id is None or s.node_id == node_id)
|
|
1409
|
+
]
|
|
1410
|
+
for sid in stale:
|
|
1411
|
+
self._event_subs.pop(sid, None)
|
|
1412
|
+
|
|
1413
|
+
def _drop_subscriptions_for_execution(
|
|
1414
|
+
self, pi: ProcessInstance, execution_id: str
|
|
1415
|
+
) -> None:
|
|
1416
|
+
"""撤销挂在某 execution 上的全部订阅(catch 停等 / 宿主等待 / esc 宿主)。
|
|
1417
|
+
|
|
1418
|
+
供杀灭/取消路径使用(kill 树逐节点调用;宿主离开由 _drop_boundary_jobs
|
|
1419
|
+
专项撤销)。
|
|
1420
|
+
"""
|
|
1421
|
+
stale = [
|
|
1422
|
+
sid
|
|
1423
|
+
for sid, s in self._event_subs.items()
|
|
1424
|
+
if s.process_instance_id == pi.id and s.execution_id == execution_id
|
|
1425
|
+
]
|
|
1426
|
+
for sid in stale:
|
|
1427
|
+
self._event_subs.pop(sid, None)
|
|
1428
|
+
|
|
1429
|
+
def _fire_timer_event_start(self, job: Job) -> None:
|
|
1430
|
+
"""timer 事件子流程到期:宿主 scope 仍激活则触发(中断/非中断随 start)。
|
|
1431
|
+
|
|
1432
|
+
触发即消费订阅:中断式撤该容器全部订阅(宿主被接管/取消);非中断式只撤
|
|
1433
|
+
当前 timer start 的订阅(单发,同容器其它 esc 订阅保留)。宿主已失效
|
|
1434
|
+
(结束/离开停驻/被其它路径接管)-> 过期作业直接丢弃。
|
|
1435
|
+
"""
|
|
1436
|
+
pi = self._instances.get(job.process_instance_id)
|
|
1437
|
+
host = pi.executions.get(job.execution_id) if pi is not None else None
|
|
1438
|
+
if (
|
|
1439
|
+
pi is None
|
|
1440
|
+
or pi.is_completed
|
|
1441
|
+
or host is None
|
|
1442
|
+
or host.state != ExecutionState.ACTIVE
|
|
1443
|
+
):
|
|
1444
|
+
self._jobs.pop(job.id, None)
|
|
1445
|
+
return
|
|
1446
|
+
# 容器由订阅自身携带:None = 根 Process;否则 = sub_id 对应 sub 的 inner。
|
|
1447
|
+
# 一致性校验:sub 级订阅仅在 host 仍以 SCOPE 停驻同一 sub 时有效
|
|
1448
|
+
root_proc = self._definitions[pi.process_definition_key]
|
|
1449
|
+
if job.activity_id is None:
|
|
1450
|
+
container = root_proc
|
|
1451
|
+
else:
|
|
1452
|
+
if host.role != "SCOPE" or host.activity_id != job.activity_id:
|
|
1453
|
+
self._jobs.pop(job.id, None) # 宿主已离开订阅容器(收束/被接管)
|
|
1454
|
+
return
|
|
1455
|
+
outer = self._container_of(pi, host)
|
|
1456
|
+
parked = outer.flow_nodes.get(job.activity_id)
|
|
1457
|
+
if not isinstance(parked, SubProcess):
|
|
1458
|
+
self._jobs.pop(job.id, None)
|
|
1459
|
+
return
|
|
1460
|
+
container = parked.process
|
|
1461
|
+
event_sub: Optional[SubProcess] = None
|
|
1462
|
+
start: Optional[StartEvent] = None
|
|
1463
|
+
for sub in container.flow_nodes.values():
|
|
1464
|
+
if not (isinstance(sub, SubProcess) and sub.triggered_by_event):
|
|
1465
|
+
continue
|
|
1466
|
+
inner = sub.process
|
|
1467
|
+
if inner is None:
|
|
1468
|
+
continue
|
|
1469
|
+
for st in inner.start_events:
|
|
1470
|
+
if st.id == job.node_id and st.timer is not None:
|
|
1471
|
+
event_sub, start = sub, st
|
|
1472
|
+
break
|
|
1473
|
+
if event_sub is None or start is None:
|
|
1474
|
+
self._jobs.pop(job.id, None) # 防御:订阅目标已不存在(正常不会发生)
|
|
1475
|
+
return
|
|
1476
|
+
# 触发:中断式先取消宿主 scope 其它执行,再 spawn 事件子流程
|
|
1477
|
+
if start.is_interrupting:
|
|
1478
|
+
if job.activity_id is None:
|
|
1479
|
+
self._interrupt_instance(pi) # 流程级:清 root 全部实例级作业
|
|
1480
|
+
else:
|
|
1481
|
+
self._kill_subprocess_scope(pi, host)
|
|
1482
|
+
# 订阅消费:中断式撤容器全部(宿主被取消,同容器订阅一并失效);
|
|
1483
|
+
# 非中断式只撤当前 timer start(单发),同容器其它 esc 订阅保留
|
|
1484
|
+
if start.is_interrupting:
|
|
1485
|
+
self._drop_scope_event_jobs(pi, host, job.activity_id)
|
|
1486
|
+
# M4-2d:同容器 message/signal esc 订阅一并失效(中断语义与 timer 一致)
|
|
1487
|
+
self._drop_scope_event_subscriptions(pi, host, job.activity_id)
|
|
1488
|
+
else:
|
|
1489
|
+
self._drop_scope_event_jobs(pi, host, job.activity_id, job.node_id)
|
|
1490
|
+
arrivals = self._start_event_subprocess(pi, host, event_sub, start)
|
|
1491
|
+
self._pump(pi, arrivals)
|
|
1492
|
+
|
|
1493
|
+
# ------------------------------------------------------------------
|
|
1494
|
+
# M3:timer catch 停等 / async 拆分
|
|
1495
|
+
# ------------------------------------------------------------------
|
|
1496
|
+
def _enter_event_catch(
|
|
1497
|
+
self, pi: ProcessInstance, token: Execution, node: IntermediateCatchEvent
|
|
1498
|
+
) -> List[_Arrival]:
|
|
1499
|
+
"""token 到达中间捕获事件:timer -> 注册 job 停等(M3);message/signal
|
|
1500
|
+
-> 注册事件订阅停等(M4-2d),correlate_message / throw_signal 触发。"""
|
|
1501
|
+
if node.timer is not None:
|
|
1502
|
+
return self._handle_timer_catch(pi, token, node)
|
|
1503
|
+
if node.message_name is None and node.signal_name is None:
|
|
1504
|
+
raise InvalidRequestException(
|
|
1505
|
+
f"中间捕获事件 {node.id!r} 未实现(支持 timer/message/signal 事件定义)"
|
|
1506
|
+
)
|
|
1507
|
+
self._open_activity(pi, token, node)
|
|
1508
|
+
sub = EventSubscription(
|
|
1509
|
+
id=self._idgen.next_id(),
|
|
1510
|
+
kind="message" if node.message_name is not None else "signal",
|
|
1511
|
+
event_name=node.message_name or node.signal_name,
|
|
1512
|
+
process_instance_id=pi.id,
|
|
1513
|
+
execution_id=token.id,
|
|
1514
|
+
activity_id=None,
|
|
1515
|
+
node_id=node.id,
|
|
1516
|
+
node_kind="catch",
|
|
1517
|
+
is_interrupting=True, # catch 无中断概念(触发即 token 续走)
|
|
1518
|
+
created=_now(),
|
|
1519
|
+
)
|
|
1520
|
+
self._event_subs[sub.id] = sub
|
|
1521
|
+
return [] # 停等外部触发
|
|
1522
|
+
|
|
1523
|
+
def _handle_timer_catch(
|
|
1524
|
+
self, pi: ProcessInstance, token: Execution, node: IntermediateCatchEvent
|
|
1525
|
+
) -> List[_Arrival]:
|
|
1526
|
+
"""token 到达 timer 中间捕获事件:open actinst + 注册 timer-catch job,停等。"""
|
|
1527
|
+
timer = node.timer
|
|
1528
|
+
if timer is None:
|
|
1529
|
+
raise InvalidRequestException(
|
|
1530
|
+
f"中间捕获事件 {node.id!r} 未实现(M3 仅支持 timerEventDefinition)"
|
|
1531
|
+
)
|
|
1532
|
+
if timer.kind == "cycle":
|
|
1533
|
+
raise InvalidRequestException(
|
|
1534
|
+
f"timerCycle 仅用于 timer start;catch 事件 {node.id!r} 请用 timeDuration/timeDate"
|
|
1535
|
+
)
|
|
1536
|
+
self._open_activity(pi, token, node)
|
|
1537
|
+
now = _now()
|
|
1538
|
+
if timer.kind == "duration":
|
|
1539
|
+
duedate = format_iso(parse_iso(now) + timedelta(seconds=timer.delay_seconds or 0))
|
|
1540
|
+
else: # date:绝对时间点(归一化到本地时区定长 ISO)
|
|
1541
|
+
duedate = parse_trigger_date(timer.value)
|
|
1542
|
+
job = Job(
|
|
1543
|
+
id=self._idgen.next_id(),
|
|
1544
|
+
job_type="timer-catch",
|
|
1545
|
+
duedate=duedate,
|
|
1546
|
+
created=now,
|
|
1547
|
+
process_instance_id=pi.id,
|
|
1548
|
+
execution_id=token.id,
|
|
1549
|
+
node_id=node.id,
|
|
1550
|
+
)
|
|
1551
|
+
self._jobs[job.id] = job
|
|
1552
|
+
return [] # 停等 job 到期
|
|
1553
|
+
|
|
1554
|
+
def _schedule_async_before(self, pi: ProcessInstance, token: Execution, node: FlowNode) -> bool:
|
|
1555
|
+
"""asyncBefore 拆分:open actinst + 立即可执行的 async-continuation job。
|
|
1556
|
+
|
|
1557
|
+
返回 True = 已拆分停等(调用方应停止本轮推进)。actinst 保持 open,
|
|
1558
|
+
待 async job 执行行为时由 _open_activity 复用、行为完成后 close。
|
|
1559
|
+
asyncBefore 使节点获得等待窗口 -> 同步在此注册边界 timer(若宿主挂了
|
|
1560
|
+
边界事件;行为执行完成离开宿主时由调用方撤销)。
|
|
1561
|
+
"""
|
|
1562
|
+
self._open_activity(pi, token, node)
|
|
1563
|
+
now = _now()
|
|
1564
|
+
job = Job(
|
|
1565
|
+
id=self._idgen.next_id(),
|
|
1566
|
+
job_type="async-continuation",
|
|
1567
|
+
duedate=now, # 立即到期(async continuation 无额外延迟)
|
|
1568
|
+
created=now,
|
|
1569
|
+
process_instance_id=pi.id,
|
|
1570
|
+
execution_id=token.id,
|
|
1571
|
+
node_id=node.id,
|
|
1572
|
+
)
|
|
1573
|
+
self._jobs[job.id] = job
|
|
1574
|
+
self._register_boundary_jobs(pi, token, node)
|
|
1575
|
+
return True
|
|
1576
|
+
|
|
1577
|
+
def _schedule_async_after(self, pi: ProcessInstance, token: Execution, node: FlowNode) -> None:
|
|
1578
|
+
"""asyncAfter 拆分:节点行为已完成(actinst 结算),离开推进拆成独立 job。
|
|
1579
|
+
|
|
1580
|
+
Camunda asyncAfter 语义:行为执行与 token 沿出边流转之间插入异步作业。
|
|
1581
|
+
serviceTask:行为(delegate)在拆分前已执行,job 到期只做离开;XOR:网关
|
|
1582
|
+
无副作用,选路本身推迟到 job 到期(此时重新求值出边条件)。asyncBefore +
|
|
1583
|
+
asyncAfter 链式:行为在 async-continuation job 中执行,完成后同样拆本 job。
|
|
1584
|
+
"""
|
|
1585
|
+
now = _now()
|
|
1586
|
+
job = Job(
|
|
1587
|
+
id=self._idgen.next_id(),
|
|
1588
|
+
job_type="async-after",
|
|
1589
|
+
duedate=now, # 立即到期(async continuation 无额外延迟)
|
|
1590
|
+
created=now,
|
|
1591
|
+
process_instance_id=pi.id,
|
|
1592
|
+
execution_id=token.id,
|
|
1593
|
+
node_id=node.id,
|
|
1594
|
+
)
|
|
1595
|
+
self._jobs[job.id] = job
|
|
1596
|
+
|
|
1597
|
+
# ------------------------------------------------------------------
|
|
1598
|
+
# M4-1:timer 边界事件(中断式 interrupting;宿主 = 有等待点的活动)
|
|
1599
|
+
# ------------------------------------------------------------------
|
|
1600
|
+
def _register_boundary_jobs(
|
|
1601
|
+
self, pi: ProcessInstance, token: Execution, host: FlowNode
|
|
1602
|
+
) -> None:
|
|
1603
|
+
"""token 停等宿主活动(userTask / asyncBefore 拆分)时注册边界 timer 作业。
|
|
1604
|
+
|
|
1605
|
+
作业语义:宿主等待期内到点触发——中断式(cancelActivity=true)取消宿主、
|
|
1606
|
+
token 改走边界事件出边;非中断式(cancelActivity=false,M4-2b4)不取消
|
|
1607
|
+
宿主,spawn 并发线从边界事件出边推进(宿主与并发线全收束实例才结束)。
|
|
1608
|
+
宿主正常离开(complete / async 行为完成)时由调用方 _drop_boundary_jobs
|
|
1609
|
+
撤销 —— 边界事件只对「仍在等待的宿主活动」有效。
|
|
1610
|
+
|
|
1611
|
+
支持范围(文档化差异,见 docs/ARCHITECTURE.md):
|
|
1612
|
+
- 中断式:userTask / asyncBefore 节点(M4-1)、subProcess(M4-2a3)
|
|
1613
|
+
- 非中断式(cancelActivity=false):userTask / asyncBefore 等待活动宿主
|
|
1614
|
+
(M4-2b4);subProcess 宿主 + 非中断仍明确报错(并发线需脱离 sub 容器
|
|
1615
|
+
身份挂父 scope,root 兼任载体时存在容器推导歧义,暂缓——文档化差异)
|
|
1616
|
+
- 事件变体:timer(M4-1,仅 timeDuration / timeDate;timeCycle 拒绝)、
|
|
1617
|
+
message / signal(M4-2d——注册为常驻订阅,触发后中断式随宿主撤销、
|
|
1618
|
+
非中断式保留可再触发;timer 系走 job 单发)
|
|
1619
|
+
- 宿主必须是有等待点的活动/容器:userTask / asyncBefore 节点
|
|
1620
|
+
(M4-1)、subProcess(M4-2a,等待窗口 = 整段内部执行)。同步节点
|
|
1621
|
+
(无 asyncBefore 的 serviceTask 等)没有等待窗口,本方法不会被调用
|
|
1622
|
+
—— 对齐 Camunda:同步活动在单命令内完成,边界事件无法插入中断。
|
|
1623
|
+
"""
|
|
1624
|
+
proc = self._container_of(pi, token)
|
|
1625
|
+
existing = {
|
|
1626
|
+
j.node_id
|
|
1627
|
+
for j in self._jobs.values()
|
|
1628
|
+
if j.process_instance_id == pi.id and j.job_type == "timer-boundary"
|
|
1629
|
+
}
|
|
1630
|
+
existing_subs = {
|
|
1631
|
+
s.node_id
|
|
1632
|
+
for s in self._event_subs.values()
|
|
1633
|
+
if s.process_instance_id == pi.id
|
|
1634
|
+
and s.node_kind == "boundary"
|
|
1635
|
+
and s.execution_id == token.id
|
|
1636
|
+
}
|
|
1637
|
+
now = _now()
|
|
1638
|
+
for bid in host.boundary_events:
|
|
1639
|
+
boundary = proc.flow_nodes[bid]
|
|
1640
|
+
if not isinstance(boundary, BoundaryEvent):
|
|
1641
|
+
continue
|
|
1642
|
+
if not boundary.cancel_activity and isinstance(host, SubProcess):
|
|
1643
|
+
raise InvalidRequestException(
|
|
1644
|
+
f"boundaryEvent {boundary.id!r} 声明 cancelActivity=false(非中断式)"
|
|
1645
|
+
f"且宿主 {host.id!r} 是 subProcess:M4-2b4 支持普通等待活动宿主"
|
|
1646
|
+
"(userTask / asyncBefore),subProcess 宿主非中断式边界暂不支持"
|
|
1647
|
+
"(文档化差异)"
|
|
1648
|
+
)
|
|
1649
|
+
timer = boundary.timer
|
|
1650
|
+
if boundary.id in existing or boundary.id in existing_subs:
|
|
1651
|
+
continue # 幂等:asyncBefore 拆分已注册,行为续跑不重复注册
|
|
1652
|
+
if timer is not None:
|
|
1653
|
+
if timer.kind == "cycle":
|
|
1654
|
+
raise InvalidRequestException(
|
|
1655
|
+
f"boundaryEvent {boundary.id!r} 不支持 timerCycle,请用 timeDuration/timeDate"
|
|
1656
|
+
)
|
|
1657
|
+
if timer.kind == "duration":
|
|
1658
|
+
duedate = format_iso(
|
|
1659
|
+
parse_iso(now) + timedelta(seconds=timer.delay_seconds or 0)
|
|
1660
|
+
)
|
|
1661
|
+
else: # date:绝对时间点
|
|
1662
|
+
duedate = parse_trigger_date(timer.value)
|
|
1663
|
+
job = Job(
|
|
1664
|
+
id=self._idgen.next_id(),
|
|
1665
|
+
job_type="timer-boundary",
|
|
1666
|
+
duedate=duedate,
|
|
1667
|
+
created=now,
|
|
1668
|
+
process_instance_id=pi.id,
|
|
1669
|
+
execution_id=token.id,
|
|
1670
|
+
node_id=boundary.id, # 边界事件 id;宿主经 attached_to 反查
|
|
1671
|
+
)
|
|
1672
|
+
self._jobs[job.id] = job
|
|
1673
|
+
continue
|
|
1674
|
+
if boundary.message_name is not None or boundary.signal_name is not None:
|
|
1675
|
+
# M4-2d:message/signal 边界 -> 常驻订阅(触发由关联/广播入口驱动)
|
|
1676
|
+
s = EventSubscription(
|
|
1677
|
+
id=self._idgen.next_id(),
|
|
1678
|
+
kind="message" if boundary.message_name is not None else "signal",
|
|
1679
|
+
event_name=boundary.message_name or boundary.signal_name,
|
|
1680
|
+
process_instance_id=pi.id,
|
|
1681
|
+
execution_id=token.id,
|
|
1682
|
+
activity_id=None,
|
|
1683
|
+
node_id=boundary.id,
|
|
1684
|
+
node_kind="boundary",
|
|
1685
|
+
is_interrupting=boundary.cancel_activity,
|
|
1686
|
+
created=now,
|
|
1687
|
+
)
|
|
1688
|
+
self._event_subs[s.id] = s
|
|
1689
|
+
continue
|
|
1690
|
+
raise InvalidRequestException(
|
|
1691
|
+
f"boundaryEvent {boundary.id!r} 未实现(支持 timer/message/signal 事件定义)"
|
|
1692
|
+
)
|
|
1693
|
+
|
|
1694
|
+
def _drop_boundary_jobs(self, pi: ProcessInstance, host: FlowNode) -> None:
|
|
1695
|
+
"""宿主活动正常离开/被取消:删除其全部边界 timer 作业与消息/信号订阅。
|
|
1696
|
+
|
|
1697
|
+
M4-2a:宿主对象直接传入(调用方都持有),无需按 id 反查容器。
|
|
1698
|
+
M4-2d:边界 message/signal 订阅同窗同步撤销(宿主不再等待即失效)。
|
|
1699
|
+
"""
|
|
1700
|
+
bound = set(host.boundary_events)
|
|
1701
|
+
if not bound:
|
|
1702
|
+
return
|
|
1703
|
+
stale = [
|
|
1704
|
+
jid
|
|
1705
|
+
for jid, j in self._jobs.items()
|
|
1706
|
+
if j.process_instance_id == pi.id
|
|
1707
|
+
and j.job_type == "timer-boundary"
|
|
1708
|
+
and j.node_id in bound
|
|
1709
|
+
]
|
|
1710
|
+
for jid in stale:
|
|
1711
|
+
self._jobs.pop(jid, None)
|
|
1712
|
+
stale_subs = [
|
|
1713
|
+
sid
|
|
1714
|
+
for sid, s in self._event_subs.items()
|
|
1715
|
+
if s.process_instance_id == pi.id
|
|
1716
|
+
and s.node_kind == "boundary"
|
|
1717
|
+
and s.node_id in bound
|
|
1718
|
+
]
|
|
1719
|
+
for sid in stale_subs:
|
|
1720
|
+
self._event_subs.pop(sid, None)
|
|
1721
|
+
|
|
1722
|
+
def _cancel_host_activity(
|
|
1723
|
+
self, pi: ProcessInstance, token: Execution, host: FlowNode
|
|
1724
|
+
) -> None:
|
|
1725
|
+
"""中断式边界触发:取消宿主活动。
|
|
1726
|
+
|
|
1727
|
+
宿主为普通活动(userTask / asyncBefore 节点):删待办任务(归档留历史)
|
|
1728
|
+
-> 结算 actinst -> 撤销未执行 async 行为 -> 删边界 timer 作业。
|
|
1729
|
+
宿主为 subProcess(M4-2a3):整段 scope 取消 = 先结束内部全部活跃子树
|
|
1730
|
+
(_kill_subprocess_scope),再结算 subProcess actinst 并清理其作业。
|
|
1731
|
+
"""
|
|
1732
|
+
now = _now()
|
|
1733
|
+
if isinstance(host, SubProcess):
|
|
1734
|
+
self._kill_subprocess_scope(pi, token)
|
|
1735
|
+
else:
|
|
1736
|
+
# 宿主 userTask 的待办任务归档(end_time 结算 -> HI_TASKINST 留痕)
|
|
1737
|
+
for tid in [
|
|
1738
|
+
t.id
|
|
1739
|
+
for t in self._tasks.values()
|
|
1740
|
+
if t.process_instance_id == pi.id
|
|
1741
|
+
and t.execution_id == token.id
|
|
1742
|
+
and t.task_definition_key == host.id
|
|
1743
|
+
]:
|
|
1744
|
+
task = self._tasks.pop(tid)
|
|
1745
|
+
task.end_time = now
|
|
1746
|
+
pi.completed_tasks.append(task)
|
|
1747
|
+
# 2) 结算宿主活动实例(中断 = 宿主活动结束;subProcess actinst 亦在此结算)
|
|
1748
|
+
self._close_activity(pi, token, host)
|
|
1749
|
+
# 3) 撤销宿主未执行的 asyncBefore 行为(拆分后行为还没跑,作废)
|
|
1750
|
+
for jid in [
|
|
1751
|
+
j.id
|
|
1752
|
+
for j in self._jobs.values()
|
|
1753
|
+
if j.process_instance_id == pi.id
|
|
1754
|
+
and j.job_type == "async-continuation"
|
|
1755
|
+
and j.execution_id == token.id
|
|
1756
|
+
and j.node_id == host.id
|
|
1757
|
+
]:
|
|
1758
|
+
del self._jobs[jid]
|
|
1759
|
+
# 4) 宿主全部边界 timer 作业/消息信号订阅失效(含当前触发者自身)
|
|
1760
|
+
self._drop_boundary_jobs(pi, host)
|
|
1761
|
+
# 5) M4-2b3/M4-2d:宿主 subProcess 被边界中断 -> 其容器内 timer 事件子流程
|
|
1762
|
+
# 订阅及 message/signal 订阅一并失效。普通活动宿主没有容器订阅;
|
|
1763
|
+
# root 兼任 sub 宿主时只撤该 sub 容器的订阅,root 上流程级订阅保留
|
|
1764
|
+
if isinstance(host, SubProcess):
|
|
1765
|
+
self._drop_scope_event_jobs(pi, token, token.activity_id)
|
|
1766
|
+
self._drop_scope_event_subscriptions(pi, token, token.activity_id)
|
|
1767
|
+
|
|
1768
|
+
def _kill_subprocess_scope(self, pi: ProcessInstance, scope: Execution) -> None:
|
|
1769
|
+
"""中断式 scope 取消:结束 subProcess 内部全部活跃子树(本体除外)。
|
|
1770
|
+
|
|
1771
|
+
scope 本体(subProcess SCOPE)保持 ACTIVE 由调用方结算 actinst 后沿边界
|
|
1772
|
+
出边走。逐棵子树复用 _kill_execution_tree(actinst/task/job/join 全清理)。
|
|
1773
|
+
"""
|
|
1774
|
+
for c in list(scope.children):
|
|
1775
|
+
self._kill_execution_tree(pi, c)
|
|
1776
|
+
|
|
1777
|
+
def _kill_execution_tree(self, pi: ProcessInstance, e: Execution) -> None:
|
|
1778
|
+
"""杀灭以 e 为根的整棵执行子树(含 e 本体),全量清理。
|
|
1779
|
+
|
|
1780
|
+
自底向上 kill 每个内部 execution:结算 open actinst、归档待办任务、
|
|
1781
|
+
删除所属实例级作业、从 join_arrivals 摘除登记、detach 摘树。e 本体
|
|
1782
|
+
同样结算/ENDED/detach。供中断取消(MI 条件终止实例 / 事件中断宿主)。
|
|
1783
|
+
"""
|
|
1784
|
+
now = _now()
|
|
1785
|
+
|
|
1786
|
+
def kill(node: Execution) -> None:
|
|
1787
|
+
for c in list(node.children):
|
|
1788
|
+
kill(c)
|
|
1789
|
+
# 结算未结算的活动实例
|
|
1790
|
+
if node.open_activity is not None and node.open_activity.end_time is None:
|
|
1791
|
+
node.open_activity.end_time = now
|
|
1792
|
+
node.open_activity = None
|
|
1793
|
+
# 该 execution 的待办任务归档(中断取消 = HI_TASKINST 留痕)
|
|
1794
|
+
for t in [
|
|
1795
|
+
t
|
|
1796
|
+
for t in self._tasks.values()
|
|
1797
|
+
if t.process_instance_id == pi.id and t.execution_id == node.id
|
|
1798
|
+
]:
|
|
1799
|
+
self._tasks.pop(t.id)
|
|
1800
|
+
t.end_time = now
|
|
1801
|
+
pi.completed_tasks.append(t)
|
|
1802
|
+
# 该 execution 的实例级作业全部作废
|
|
1803
|
+
for j in [
|
|
1804
|
+
j
|
|
1805
|
+
for j in self._jobs.values()
|
|
1806
|
+
if j.process_instance_id == pi.id and j.execution_id == node.id
|
|
1807
|
+
]:
|
|
1808
|
+
self._jobs.pop(j.id)
|
|
1809
|
+
# M4-2d:该 execution 承载的消息/信号订阅一并作废
|
|
1810
|
+
# (catch 停等 token / esc 宿主 scope / 边界宿主)
|
|
1811
|
+
for sid in [
|
|
1812
|
+
sid
|
|
1813
|
+
for sid, s in self._event_subs.items()
|
|
1814
|
+
if s.process_instance_id == pi.id and s.execution_id == node.id
|
|
1815
|
+
]:
|
|
1816
|
+
self._event_subs.pop(sid, None)
|
|
1817
|
+
# 从并行 join 等待登记摘除(内部登记随子树作废)
|
|
1818
|
+
for gw, ids in list(pi.join_arrivals.items()):
|
|
1819
|
+
if node.id in ids:
|
|
1820
|
+
rest = [i for i in ids if i != node.id]
|
|
1821
|
+
if rest:
|
|
1822
|
+
pi.join_arrivals[gw] = rest
|
|
1823
|
+
else:
|
|
1824
|
+
pi.join_arrivals.pop(gw, None)
|
|
1825
|
+
node.state = ExecutionState.ENDED
|
|
1826
|
+
self._detach_from_parent(pi, node)
|
|
1827
|
+
|
|
1828
|
+
kill(e)
|
|
1829
|
+
|
|
1830
|
+
def _fire_timer_boundary(self, job: Job) -> None:
|
|
1831
|
+
"""timer 边界到期:中断式取消宿主 / 非中断式 spawn 并发线(M4-2b4)。
|
|
1832
|
+
|
|
1833
|
+
token 失效防御与 timer-catch 同:宿主已离开/活动已结算 = 过期作业直接
|
|
1834
|
+
丢弃(宿主正常离开时边界 job 本应被撤销,此处兜底并发轮询竞态)。
|
|
1835
|
+
"""
|
|
1836
|
+
pi = self._instances.get(job.process_instance_id)
|
|
1837
|
+
token = pi.executions.get(job.execution_id) if pi is not None else None
|
|
1838
|
+
if (
|
|
1839
|
+
pi is None
|
|
1840
|
+
or pi.is_completed
|
|
1841
|
+
or token is None
|
|
1842
|
+
or token.state != ExecutionState.ACTIVE
|
|
1843
|
+
):
|
|
1844
|
+
self._jobs.pop(job.id, None) # 实例/执行已失效 -> 过期作业丢弃
|
|
1845
|
+
return
|
|
1846
|
+
proc = self._container_of(pi, token)
|
|
1847
|
+
boundary = proc.flow_nodes.get(job.node_id)
|
|
1848
|
+
host = (
|
|
1849
|
+
proc.flow_nodes.get(boundary.attached_to)
|
|
1850
|
+
if isinstance(boundary, BoundaryEvent) and boundary.attached_to
|
|
1851
|
+
else None
|
|
1852
|
+
)
|
|
1853
|
+
if host is None:
|
|
1854
|
+
self._jobs.pop(job.id, None) # 防御:异常数据(正常解析后不会发生)
|
|
1855
|
+
return
|
|
1856
|
+
# 关键校验:宿主活动仍在等待(actinst 未结算)才可触发
|
|
1857
|
+
if (
|
|
1858
|
+
token.activity_id != host.id
|
|
1859
|
+
or token.open_activity is None
|
|
1860
|
+
or token.open_activity.end_time is not None
|
|
1861
|
+
):
|
|
1862
|
+
self._jobs.pop(job.id, None) # 宿主已离开 -> 过期作业丢弃
|
|
1863
|
+
return
|
|
1864
|
+
if boundary.cancel_activity:
|
|
1865
|
+
# 中断式:取消宿主活动,token 改走边界事件出边
|
|
1866
|
+
self._cancel_host_activity(pi, token, host)
|
|
1867
|
+
# 边界事件作为中断路径载体:留 actinst 痕迹后沿其出边推进
|
|
1868
|
+
self._open_activity(pi, token, boundary)
|
|
1869
|
+
self._close_activity(pi, token, boundary)
|
|
1870
|
+
arrivals: List[_Arrival] = []
|
|
1871
|
+
self._leave(pi, token, boundary, arrivals)
|
|
1872
|
+
self._pump(pi, arrivals)
|
|
1873
|
+
else:
|
|
1874
|
+
# 非中断式(M4-2b4):宿主不取消,spawn 并发线从边界事件出边走
|
|
1875
|
+
self._spawn_non_interrupting_boundary(pi, token, boundary)
|
|
1876
|
+
|
|
1877
|
+
def _spawn_non_interrupting_boundary(
|
|
1878
|
+
self, pi: ProcessInstance, token: Execution, boundary: BoundaryEvent
|
|
1879
|
+
) -> None:
|
|
1880
|
+
"""非中断式边界触发:宿主保留,并发线(与宿主平级)走边界事件出边。
|
|
1881
|
+
|
|
1882
|
+
并发线是独立执行,不能挂在宿主 token 之下(否则宿主 complete/收束时被
|
|
1883
|
+
携带或误判)。挂载点 = 宿主直接父 scope;root 直通宿主无父 -> 挂 root
|
|
1884
|
+
(root 兼任实例 scope 与并发线父载体,主线到 end 后转 SCOPE 停驻等收束,
|
|
1885
|
+
由 _collapse_scopes 收尾——见 _end_token / M4-2b 收尾段)。
|
|
1886
|
+
|
|
1887
|
+
触发即消费本 timer job(单发,无 repeat -> _reschedule_or_remove 删除),
|
|
1888
|
+
宿主其余边界作业保留——宿主仍在等待,其它 timer 边界继续有效。
|
|
1889
|
+
"""
|
|
1890
|
+
parent = pi.executions.get(token.parent_id) if token.parent_id else None
|
|
1891
|
+
anchor = parent if parent is not None else token
|
|
1892
|
+
line = Execution(
|
|
1893
|
+
id=self._idgen.next_id(),
|
|
1894
|
+
process_instance_id=pi.id,
|
|
1895
|
+
parent_id=anchor.id,
|
|
1896
|
+
)
|
|
1897
|
+
pi.executions[line.id] = line
|
|
1898
|
+
anchor.children.append(line)
|
|
1899
|
+
# 边界事件在并发线上留 actinst 痕迹后沿其出边推进(无出边即收束)
|
|
1900
|
+
self._open_activity(pi, line, boundary)
|
|
1901
|
+
self._close_activity(pi, line, boundary)
|
|
1902
|
+
arrivals: List[_Arrival] = []
|
|
1903
|
+
self._leave(pi, line, boundary, arrivals)
|
|
1904
|
+
self._pump(pi, arrivals)
|
|
1905
|
+
|
|
1906
|
+
# ------------------------------------------------------------------
|
|
1907
|
+
# M4-2c:多实例(multiInstanceLoopCharacteristics)
|
|
1908
|
+
# ------------------------------------------------------------------
|
|
1909
|
+
def _enter_user_task_wait(
|
|
1910
|
+
self, pi: ProcessInstance, token: Execution, node: UserTask
|
|
1911
|
+
) -> List[_Arrival]:
|
|
1912
|
+
"""userTask 宿主进入等待:open actinst + 创建任务 + 注册边界 timer。
|
|
1913
|
+
|
|
1914
|
+
MI 实例启动(_start_mi_instance)复用本方法承载宿主行为。
|
|
1915
|
+
"""
|
|
1916
|
+
self._open_activity(pi, token, node)
|
|
1917
|
+
self._create_task(pi, token, node)
|
|
1918
|
+
# 宿主进入等待:注册边界 timer(asyncBefore 的 userTask 拆分时已注册,
|
|
1919
|
+
# 行为续跑到达此处不重复注册——幂等由 _register_boundary_jobs 保证)
|
|
1920
|
+
if not node.async_before:
|
|
1921
|
+
self._register_boundary_jobs(pi, token, node)
|
|
1922
|
+
return [] # 停等 complete
|
|
1923
|
+
|
|
1924
|
+
def _enter_multi_instance(
|
|
1925
|
+
self, pi: ProcessInstance, token: Execution, node: FlowNode
|
|
1926
|
+
) -> List[_Arrival]:
|
|
1927
|
+
"""token 到达多实例宿主活动:初始化 MI 容器并启动实例(M4-2c)。
|
|
1928
|
+
|
|
1929
|
+
纯 MI 范围(文档化差异):宿主不允许再组合 asyncBefore/asyncAfter/
|
|
1930
|
+
边界事件(组合语义后置里程碑)。实例集求值为空 -> 零实例,立即沿宿主
|
|
1931
|
+
出边离开(对齐 Camunda 空集合行为)。
|
|
1932
|
+
- sequential:token 自身作容器与实例载体,_start_mi_instance 顺序启动;
|
|
1933
|
+
- parallel:token 转 SCOPE 作容器,spawn 每条实例 child execution。
|
|
1934
|
+
"""
|
|
1935
|
+
mi_def = node.multi_instance
|
|
1936
|
+
if node.async_before or node.async_after or node.boundary_events:
|
|
1937
|
+
raise InvalidRequestException(
|
|
1938
|
+
f"多实例宿主 {node.id!r} 暂不支持 asyncBefore/asyncAfter/边界事件"
|
|
1939
|
+
"组合(M4-2c 纯多实例语义,组合后续里程碑落地,文档化差异)"
|
|
1940
|
+
)
|
|
1941
|
+
items, total = self._resolve_mi_set(pi, mi_def)
|
|
1942
|
+
if total == 0:
|
|
1943
|
+
# 空集合:零次实例,直接沿宿主出边离开
|
|
1944
|
+
arrivals: List[_Arrival] = []
|
|
1945
|
+
self._leave(pi, token, node, arrivals)
|
|
1946
|
+
self._pump(pi, arrivals)
|
|
1947
|
+
return []
|
|
1948
|
+
container = {
|
|
1949
|
+
"total": total,
|
|
1950
|
+
"active": 0,
|
|
1951
|
+
"completed": 0,
|
|
1952
|
+
"next_index": 0,
|
|
1953
|
+
"items": items, # None = loopCardinality 来源(无元素变量)
|
|
1954
|
+
"element_variable": mi_def.element_variable,
|
|
1955
|
+
"completion_condition": mi_def.completion_condition_expr,
|
|
1956
|
+
"sequential": mi_def.sequential,
|
|
1957
|
+
}
|
|
1958
|
+
token.mi = container
|
|
1959
|
+
if mi_def.sequential:
|
|
1960
|
+
# 顺序:token 自身执行第 0 个实例(userTask 建任务停等 / subProcess
|
|
1961
|
+
# 进内部流转 / serviceTask 同步跑完全部)
|
|
1962
|
+
container["active"] = 1
|
|
1963
|
+
self._pump(pi, self._start_mi_instance(pi, token, node))
|
|
1964
|
+
return []
|
|
1965
|
+
# 并行:token 转 SCOPE 容器,spawn N 条实例 child
|
|
1966
|
+
token.role = "SCOPE"
|
|
1967
|
+
container["active"] = total
|
|
1968
|
+
arrivals: List[_Arrival] = []
|
|
1969
|
+
for _ in range(total):
|
|
1970
|
+
child = Execution(
|
|
1971
|
+
id=self._idgen.next_id(),
|
|
1972
|
+
process_instance_id=pi.id,
|
|
1973
|
+
parent_id=token.id,
|
|
1974
|
+
)
|
|
1975
|
+
pi.executions[child.id] = child
|
|
1976
|
+
token.children.append(child)
|
|
1977
|
+
arrivals.extend(self._start_mi_instance(pi, child, node))
|
|
1978
|
+
if token.mi is None:
|
|
1979
|
+
# 同步宿主(serviceTask)实例在 spawn 期间即时完成并收束容器
|
|
1980
|
+
# (全完成或 completionCondition 满足)-> 剩余实例不再启动
|
|
1981
|
+
break
|
|
1982
|
+
self._pump(pi, arrivals)
|
|
1983
|
+
return []
|
|
1984
|
+
|
|
1985
|
+
def _resolve_mi_set(
|
|
1986
|
+
self, pi: ProcessInstance, mi_def: MultiInstance
|
|
1987
|
+
) -> Tuple[Optional[List[Any]], int]:
|
|
1988
|
+
"""实例集求值:collection 表达式 -> 元素列表;loopCardinality -> 数量。
|
|
1989
|
+
|
|
1990
|
+
返回 (items, total):items=None 表示 cardinality 来源(无元素变量)。
|
|
1991
|
+
求值结果非法(非集合 / 非正整数)抛 ProcessInstanceException。
|
|
1992
|
+
"""
|
|
1993
|
+
if mi_def.collection_expr is not None:
|
|
1994
|
+
val = evaluate_expression(mi_def.collection_expr, pi.variables)
|
|
1995
|
+
if not isinstance(val, (list, tuple, set, frozenset)):
|
|
1996
|
+
raise ProcessInstanceException(
|
|
1997
|
+
f"多实例 collection {mi_def.collection_expr!r} 求值须得集合"
|
|
1998
|
+
f"(list/tuple/set),实际: {type(val).__name__}"
|
|
1999
|
+
)
|
|
2000
|
+
items = list(val)
|
|
2001
|
+
return items, len(items)
|
|
2002
|
+
if mi_def.loop_cardinality_expr is not None:
|
|
2003
|
+
n = evaluate_expression(mi_def.loop_cardinality_expr, pi.variables)
|
|
2004
|
+
if isinstance(n, bool) or not isinstance(n, int) or n < 0:
|
|
2005
|
+
raise ProcessInstanceException(
|
|
2006
|
+
f"多实例 loopCardinality {mi_def.loop_cardinality_expr!r} "
|
|
2007
|
+
f"求值须得非负整数,实际: {n!r}"
|
|
2008
|
+
)
|
|
2009
|
+
return None, int(n)
|
|
2010
|
+
return None, 0 # 解析期保证两者至少其一;双 None 防御 = 空
|
|
2011
|
+
|
|
2012
|
+
def _start_mi_instance(
|
|
2013
|
+
self, pi: ProcessInstance, execution: Execution, node: FlowNode
|
|
2014
|
+
) -> List[_Arrival]:
|
|
2015
|
+
"""启动下一个实例的宿主行为(容器 next_index 自增),返回后续到达事件。
|
|
2016
|
+
|
|
2017
|
+
行为前注入 loopCounter / elementVariable 到实例变量表(行为期可读)。
|
|
2018
|
+
元素变量与 loopCounter 生命周期 = MI 活动执行期(M4-2c 文档化差异:
|
|
2019
|
+
不落 ACT_HI_VARINST,容器收尾统一清理)。并行 child 在此登记实例序号
|
|
2020
|
+
execution.mi={"index": i},收束回报计数用。
|
|
2021
|
+
|
|
2022
|
+
M4-2c3 宿主分派:
|
|
2023
|
+
- userTask:进入等待(建任务停等,complete_task 驱动实例完成);
|
|
2024
|
+
- serviceTask:同步 delegate 无等待窗口 -> 行为完成即结算实例(顺序容器
|
|
2025
|
+
就地循环跑完剩余实例;并行 child 立即完成回报);
|
|
2026
|
+
- subProcess:进入内部流转(等待窗口 = 整段内部执行,collapse 收束链
|
|
2027
|
+
驱动实例完成)。
|
|
2028
|
+
"""
|
|
2029
|
+
arrivals: List[_Arrival] = []
|
|
2030
|
+
container = (
|
|
2031
|
+
execution.mi
|
|
2032
|
+
if execution.is_mi_container
|
|
2033
|
+
else pi.executions.get(execution.parent_id).mi
|
|
2034
|
+
)
|
|
2035
|
+
# 实例执行同样携带宿主活动位置(对齐 _handle_arrival 的 activity_id
|
|
2036
|
+
# 约定:停等/行为期 activity_id 指向当前活动节点)。顺序容器 token 已由
|
|
2037
|
+
# _handle_arrival 设置(幂等),并行 spawn 的 child 在此补齐。
|
|
2038
|
+
if not execution.is_mi_container:
|
|
2039
|
+
execution.activity_id = node.id
|
|
2040
|
+
index = container["next_index"]
|
|
2041
|
+
container["next_index"] += 1
|
|
2042
|
+
if not execution.is_mi_container:
|
|
2043
|
+
execution.mi = {"index": index} # parallel 实例标识
|
|
2044
|
+
self._inject_mi_vars(pi, container, index)
|
|
2045
|
+
if isinstance(node, UserTask):
|
|
2046
|
+
self._enter_user_task_wait(pi, execution, node)
|
|
2047
|
+
elif isinstance(node, ServiceTask):
|
|
2048
|
+
# 同步宿主:无等待窗口,行为执行完即结算实例
|
|
2049
|
+
if execution.is_mi_container:
|
|
2050
|
+
# 顺序容器:token 兼实例载体,就地循环跑完剩余实例
|
|
2051
|
+
arrivals = self._run_sequential_service_mi(pi, execution, node)
|
|
2052
|
+
else:
|
|
2053
|
+
self._run_sync_mi_host(pi, execution, node)
|
|
2054
|
+
arrivals = self._complete_mi_instance(
|
|
2055
|
+
pi, execution, node, pi.executions[execution.parent_id]
|
|
2056
|
+
)
|
|
2057
|
+
elif isinstance(node, SubProcess):
|
|
2058
|
+
# subProcess 宿主:进入内部流转(本实例的等待窗口),内部走完由
|
|
2059
|
+
# collapse 收束链驱动实例完成
|
|
2060
|
+
arrivals = self._enter_subprocess(pi, execution, node)
|
|
2061
|
+
else:
|
|
2062
|
+
raise ProcessInstanceException(
|
|
2063
|
+
f"多实例宿主 {node.id!r} 类型 {type(node).__name__} 不支持"
|
|
2064
|
+
"(M4-2c 纯 MI 范围:userTask / serviceTask / subProcess)"
|
|
2065
|
+
)
|
|
2066
|
+
return arrivals
|
|
2067
|
+
|
|
2068
|
+
def _run_sync_mi_host(
|
|
2069
|
+
self, pi: ProcessInstance, execution: Execution, node: ServiceTask
|
|
2070
|
+
) -> None:
|
|
2071
|
+
"""同步 MI 实例行为:open actinst -> delegate -> close(无等待窗口)。"""
|
|
2072
|
+
self._open_activity(pi, execution, node)
|
|
2073
|
+
self._run_delegate(pi, execution, node)
|
|
2074
|
+
self._close_activity(pi, execution, node)
|
|
2075
|
+
|
|
2076
|
+
def _run_sequential_service_mi(
|
|
2077
|
+
self, pi: ProcessInstance, scope: Execution, node: ServiceTask
|
|
2078
|
+
) -> List[_Arrival]:
|
|
2079
|
+
"""顺序 serviceTask 宿主:当前实例已就绪,同步就地循环跑完剩余实例。
|
|
2080
|
+
|
|
2081
|
+
同步无等待 -> 一条调用链内依次执行各实例:跑完 delegate 即结算计数/条件;
|
|
2082
|
+
全部完成或 completionCondition 满足 -> 容器收束沿宿主出边离开(返回事件)。
|
|
2083
|
+
"""
|
|
2084
|
+
mi = scope.mi
|
|
2085
|
+
while True:
|
|
2086
|
+
self._run_sync_mi_host(pi, scope, node)
|
|
2087
|
+
mi["completed"] += 1
|
|
2088
|
+
mi["active"] = 0
|
|
2089
|
+
self._cleanup_mi_vars(pi, mi)
|
|
2090
|
+
if self._mi_done(pi, mi):
|
|
2091
|
+
return self._finish_mi_container(pi, scope, node)
|
|
2092
|
+
index = mi["next_index"]
|
|
2093
|
+
mi["next_index"] += 1
|
|
2094
|
+
mi["active"] = 1
|
|
2095
|
+
self._inject_mi_vars(pi, mi, index)
|
|
2096
|
+
|
|
2097
|
+
@staticmethod
|
|
2098
|
+
def _inject_mi_vars(
|
|
2099
|
+
pi: ProcessInstance, container: Dict[str, Any], index: int
|
|
2100
|
+
) -> None:
|
|
2101
|
+
"""把 loopCounter / elementVariable 注入实例变量表(行为期临时承载)。"""
|
|
2102
|
+
pi.variables["loopCounter"] = index
|
|
2103
|
+
ev = container["element_variable"]
|
|
2104
|
+
if ev:
|
|
2105
|
+
items = container["items"] or []
|
|
2106
|
+
pi.variables[ev] = items[index] if index < len(items) else None
|
|
2107
|
+
|
|
2108
|
+
@staticmethod
|
|
2109
|
+
def _cleanup_mi_vars(pi: ProcessInstance, container: Dict[str, Any]) -> None:
|
|
2110
|
+
pi.variables.pop("loopCounter", None)
|
|
2111
|
+
ev = container["element_variable"]
|
|
2112
|
+
if ev:
|
|
2113
|
+
pi.variables.pop(ev, None)
|
|
2114
|
+
|
|
2115
|
+
@staticmethod
|
|
2116
|
+
def _mi_condition_vars(
|
|
2117
|
+
pi: ProcessInstance, mi: Dict[str, Any]
|
|
2118
|
+
) -> Dict[str, Any]:
|
|
2119
|
+
"""completionCondition 求值环境:实例变量 + MI 内置计数器。"""
|
|
2120
|
+
env = dict(pi.variables)
|
|
2121
|
+
env["nrOfInstances"] = mi["total"]
|
|
2122
|
+
env["nrOfActiveInstances"] = mi["active"]
|
|
2123
|
+
env["nrOfCompletedInstances"] = mi["completed"]
|
|
2124
|
+
return env
|
|
2125
|
+
|
|
2126
|
+
def _mi_done(self, pi: ProcessInstance, mi: Dict[str, Any]) -> bool:
|
|
2127
|
+
"""MI 活动是否应结束:全部实例完成,或 completionCondition 满足。"""
|
|
2128
|
+
if mi["completed"] >= mi["total"]:
|
|
2129
|
+
return True
|
|
2130
|
+
cond = mi["completion_condition"]
|
|
2131
|
+
if cond:
|
|
2132
|
+
return bool(evaluate_condition(cond, self._mi_condition_vars(pi, mi)))
|
|
2133
|
+
return False
|
|
2134
|
+
|
|
2135
|
+
def _mi_scope_of(
|
|
2136
|
+
self, pi: ProcessInstance, token: Execution
|
|
2137
|
+
) -> Optional[Execution]:
|
|
2138
|
+
"""token 完成宿主行为后,其所属 MI 容器(自身顺序容器 / 父并行容器)。"""
|
|
2139
|
+
if token.is_mi_container and token.mi["sequential"]:
|
|
2140
|
+
return token
|
|
2141
|
+
parent = pi.executions.get(token.parent_id) if token.parent_id else None
|
|
2142
|
+
if (
|
|
2143
|
+
parent is not None
|
|
2144
|
+
and parent.is_mi_container
|
|
2145
|
+
and not parent.mi["sequential"]
|
|
2146
|
+
):
|
|
2147
|
+
return parent
|
|
2148
|
+
return None
|
|
2149
|
+
|
|
2150
|
+
def _complete_mi_instance(
|
|
2151
|
+
self, pi: ProcessInstance, token: Execution, node: FlowNode, scope: Execution
|
|
2152
|
+
) -> List[_Arrival]:
|
|
2153
|
+
"""MI 实例完成:计数/条件/收束/续跑,返回后续到达事件(调用方 pump)。
|
|
2154
|
+
|
|
2155
|
+
驱动来源(M4-2c2/2c3):userTask 宿主由 complete_task;serviceTask 宿主
|
|
2156
|
+
delegate 同步完成(_start_mi_instance);subProcess 宿主内部流转收束
|
|
2157
|
+
(_collapse_scopes 复活链)。续跑/收束产生的推进事件在此回传,由调用点
|
|
2158
|
+
(complete_task / collapse / spawn 循环)统一 pump,避免深层嵌套 pump。
|
|
2159
|
+
"""
|
|
2160
|
+
mi = scope.mi
|
|
2161
|
+
mi["completed"] += 1
|
|
2162
|
+
if mi["sequential"]:
|
|
2163
|
+
# 顺序:token 即容器。当前实例已完成 -> 决定续跑或收束离开
|
|
2164
|
+
mi["active"] = 0
|
|
2165
|
+
self._cleanup_mi_vars(pi, mi)
|
|
2166
|
+
if self._mi_done(pi, mi):
|
|
2167
|
+
return self._finish_mi_container(pi, scope, node)
|
|
2168
|
+
mi["active"] = 1
|
|
2169
|
+
return self._start_mi_instance(pi, scope, node) # 下一个实例
|
|
2170
|
+
# 并行:child 完成收束 -> 检查条件(满足则终止剩余活跃实例)
|
|
2171
|
+
mi["active"] = max(0, mi["active"] - 1)
|
|
2172
|
+
token.state = ExecutionState.ENDED
|
|
2173
|
+
self._detach_from_parent(pi, token)
|
|
2174
|
+
if mi["completed"] < mi["total"] and self._mi_done(pi, mi):
|
|
2175
|
+
self._kill_mi_active_children(pi, scope)
|
|
2176
|
+
if self._mi_done(pi, mi):
|
|
2177
|
+
return self._finish_mi_container(pi, scope, node)
|
|
2178
|
+
return []
|
|
2179
|
+
|
|
2180
|
+
def _kill_mi_active_children(self, pi: ProcessInstance, scope: Execution) -> None:
|
|
2181
|
+
"""completionCondition 提前满足:终止 scope 下仍活跃的并行实例。
|
|
2182
|
+
|
|
2183
|
+
对齐 Camunda:多实例活动在条件满足时结束,剩余未完成实例被取消——
|
|
2184
|
+
待办任务归档(HI_TASKINST 带 end_time)、结算 actinst、清理实例级作业。
|
|
2185
|
+
实例载体可能是叶子(userTask/serviceTask host child)也可能是子树
|
|
2186
|
+
(subProcess host 实例 scope),统一按整树杀灭处理。
|
|
2187
|
+
"""
|
|
2188
|
+
mi = scope.mi
|
|
2189
|
+
for child in list(scope.children):
|
|
2190
|
+
if child.state != ExecutionState.ACTIVE:
|
|
2191
|
+
continue
|
|
2192
|
+
# 被终止实例 = 取消而非完成:仅回落 active,不动 completed
|
|
2193
|
+
mi["active"] = max(0, mi["active"] - 1)
|
|
2194
|
+
self._kill_execution_tree(pi, child)
|
|
2195
|
+
scope.children = [
|
|
2196
|
+
c for c in scope.children if c.state == ExecutionState.ACTIVE
|
|
2197
|
+
]
|
|
2198
|
+
|
|
2199
|
+
def _finish_mi_container(
|
|
2200
|
+
self, pi: ProcessInstance, scope: Execution, node: FlowNode
|
|
2201
|
+
) -> List[_Arrival]:
|
|
2202
|
+
"""MI 活动收束:清容器状态与注入变量,恢复 TOKEN 沿宿主出边离开。
|
|
2203
|
+
|
|
2204
|
+
离开推进事件返回调用方统一 pump(调用点可能在 pump 内也可能在
|
|
2205
|
+
complete_task 等外部入口——事件回传而非嵌套 pump,避免调用链加深)。
|
|
2206
|
+
"""
|
|
2207
|
+
self._cleanup_mi_vars(pi, scope.mi)
|
|
2208
|
+
scope.mi = None
|
|
2209
|
+
scope.role = "TOKEN"
|
|
2210
|
+
arrivals: List[_Arrival] = []
|
|
2211
|
+
self._leave(pi, scope, node, arrivals)
|
|
2212
|
+
return arrivals
|
|
2213
|
+
|
|
2214
|
+
# ------------------------------------------------------------------
|
|
2215
|
+
# 并行网关 fork / join
|
|
2216
|
+
# ------------------------------------------------------------------
|
|
2217
|
+
def _handle_parallel_gateway(
|
|
2218
|
+
self, pi: ProcessInstance, token: Execution, gw: ParallelGateway
|
|
2219
|
+
) -> List[_Arrival]:
|
|
2220
|
+
proc = self._container_of(pi, token)
|
|
2221
|
+
arrivals: List[_Arrival] = []
|
|
2222
|
+
incoming = self._incoming(proc, gw)
|
|
2223
|
+
|
|
2224
|
+
# join 分支:到达数 == 入边数 才汇聚,否则停等
|
|
2225
|
+
if len(incoming) > 1:
|
|
2226
|
+
pi.register_join_arrival(gw.id, token.id)
|
|
2227
|
+
if len(pi.join_arrived(gw.id)) < len(incoming):
|
|
2228
|
+
return arrivals # 等待其余分支
|
|
2229
|
+
# 汇聚完成:记录网关活动实例,SCOPE 恢复继续
|
|
2230
|
+
self._open_activity(pi, token, gw)
|
|
2231
|
+
self._close_activity(pi, token, gw)
|
|
2232
|
+
self._end_waiting_tokens(pi, gw)
|
|
2233
|
+
scope = self._find_scope(pi, token)
|
|
2234
|
+
actor = scope if scope is not None else token
|
|
2235
|
+
# M4-2a:汇聚后 SCOPE 恢复为主线角色(否则停在普通等待节点时可能被
|
|
2236
|
+
# 收束扫描误杀);并行 fork 多出边会再次置回 SCOPE
|
|
2237
|
+
actor.role = "TOKEN"
|
|
2238
|
+
self._leave(pi, actor, gw, arrivals)
|
|
2239
|
+
return arrivals
|
|
2240
|
+
|
|
2241
|
+
# fork 分支:分裂出边
|
|
2242
|
+
self._open_activity(pi, token, gw)
|
|
2243
|
+
self._close_activity(pi, token, gw)
|
|
2244
|
+
self._leave(pi, token, gw, arrivals)
|
|
2245
|
+
return arrivals
|
|
2246
|
+
|
|
2247
|
+
def _leave(
|
|
2248
|
+
self,
|
|
2249
|
+
pi: ProcessInstance,
|
|
2250
|
+
token: Execution,
|
|
2251
|
+
node: FlowNode,
|
|
2252
|
+
arrival_list: List[_Arrival],
|
|
2253
|
+
) -> None:
|
|
2254
|
+
"""离开节点:1 条出边直通;多条出边 fork(token 转 SCOPE,spawn 子)。"""
|
|
2255
|
+
proc = self._container_of(pi, token)
|
|
2256
|
+
flows = self._outgoing(proc, node)
|
|
2257
|
+
if not flows:
|
|
2258
|
+
# 无出边(流程末端):结束 token;可能连带触发子流程 scope 收束复活
|
|
2259
|
+
arrival_list.extend(self._end_token(pi, token))
|
|
2260
|
+
return
|
|
2261
|
+
if len(flows) == 1:
|
|
2262
|
+
self._take(pi, token, flows[0], arrival_list)
|
|
2263
|
+
return
|
|
2264
|
+
# fork
|
|
2265
|
+
token.role = "SCOPE"
|
|
2266
|
+
for flow in flows:
|
|
2267
|
+
child = Execution(
|
|
2268
|
+
id=self._idgen.next_id(),
|
|
2269
|
+
process_instance_id=pi.id,
|
|
2270
|
+
parent_id=token.id,
|
|
2271
|
+
)
|
|
2272
|
+
pi.executions[child.id] = child
|
|
2273
|
+
token.children.append(child)
|
|
2274
|
+
target = proc.flow_nodes[flow.target_ref]
|
|
2275
|
+
arrival_list.append((child, target))
|
|
2276
|
+
|
|
2277
|
+
def _end_waiting_tokens(self, pi: ProcessInstance, gw: ParallelGateway) -> None:
|
|
2278
|
+
"""join 汇聚后:结束停等 token 并从父树摘除。"""
|
|
2279
|
+
for wid in pi.join_arrived(gw.id):
|
|
2280
|
+
waiting = pi.executions.get(wid)
|
|
2281
|
+
if waiting is None:
|
|
2282
|
+
continue
|
|
2283
|
+
waiting.state = ExecutionState.ENDED
|
|
2284
|
+
self._detach_from_parent(pi, waiting)
|
|
2285
|
+
pi.clear_join_arrivals(gw.id)
|
|
2286
|
+
|
|
2287
|
+
def _find_scope(self, pi: ProcessInstance, token: Execution) -> Optional[Execution]:
|
|
2288
|
+
"""向上找最近的 SCOPE 父(join 汇聚后由其承担恢复推进)。"""
|
|
2289
|
+
cur = token
|
|
2290
|
+
while cur.parent_id is not None:
|
|
2291
|
+
parent = pi.executions.get(cur.parent_id)
|
|
2292
|
+
if parent is None:
|
|
2293
|
+
break
|
|
2294
|
+
if parent.role == "SCOPE":
|
|
2295
|
+
return parent
|
|
2296
|
+
cur = parent
|
|
2297
|
+
return None
|
|
2298
|
+
|
|
2299
|
+
def _take(
|
|
2300
|
+
self,
|
|
2301
|
+
pi: ProcessInstance,
|
|
2302
|
+
token: Execution,
|
|
2303
|
+
flow: SequenceFlow,
|
|
2304
|
+
arrival_list: List[_Arrival],
|
|
2305
|
+
) -> None:
|
|
2306
|
+
proc = self._container_of(pi, token)
|
|
2307
|
+
target = proc.flow_nodes[flow.target_ref]
|
|
2308
|
+
arrival_list.append((token, target))
|
|
2309
|
+
|
|
2310
|
+
# ------------------------------------------------------------------
|
|
2311
|
+
# token 收束 / 活动历史
|
|
2312
|
+
# ------------------------------------------------------------------
|
|
2313
|
+
def _end_token(self, pi: ProcessInstance, token: Execution) -> List[_Arrival]:
|
|
2314
|
+
"""token 到达 endEvent / 无出边:结束自己并尝试收束(返回复活推进事件)。
|
|
2315
|
+
|
|
2316
|
+
根结束 = 主线走完;若 root 还有活跃子执行(非中断边界并发线 / 非中断
|
|
2317
|
+
事件子流程,M4-2b4),root 转 SCOPE 停驻等子树全收束后才完成实例
|
|
2318
|
+
(_collapse_scopes 收尾段兜底)。子 token 结束 -> 自底向上收束 SCOPE
|
|
2319
|
+
(并行分支各自走完 / 子流程内部走完),期间可能复活 subProcess SCOPE
|
|
2320
|
+
沿其出边推进。
|
|
2321
|
+
"""
|
|
2322
|
+
if token.is_root or pi.root_execution is token:
|
|
2323
|
+
alive = [
|
|
2324
|
+
c for c in token.children if c.state == ExecutionState.ACTIVE
|
|
2325
|
+
]
|
|
2326
|
+
if alive:
|
|
2327
|
+
# 主线已到 end 但并发子树未收束:root 脱离活动节点停驻(activity
|
|
2328
|
+
# 清空 -> collapse 不会把它当 sub/网关收,子树收束后收尾段完成)
|
|
2329
|
+
token.role = "SCOPE"
|
|
2330
|
+
token.activity_id = None
|
|
2331
|
+
return [] # 等子树收束;_collapse_scopes 收尾段完成实例
|
|
2332
|
+
token.state = ExecutionState.ENDED
|
|
2333
|
+
self._detach_from_parent(pi, token)
|
|
2334
|
+
self._complete_instance(pi)
|
|
2335
|
+
return []
|
|
2336
|
+
token.state = ExecutionState.ENDED
|
|
2337
|
+
self._detach_from_parent(pi, token)
|
|
2338
|
+
return self._collapse_scopes(pi)
|
|
2339
|
+
|
|
2340
|
+
def _detach_from_parent(self, pi: ProcessInstance, token: Execution) -> None:
|
|
2341
|
+
parent = pi.executions.get(token.parent_id) if token.parent_id else None
|
|
2342
|
+
if parent is not None and token in parent.children:
|
|
2343
|
+
parent.children.remove(token)
|
|
2344
|
+
|
|
2345
|
+
def _collapse_scopes(self, pi: ProcessInstance) -> List[_Arrival]:
|
|
2346
|
+
"""自底向上收束已无活跃子的 SCOPE(M4-2a 泛化:任意层,返回复活推进事件)。
|
|
2347
|
+
|
|
2348
|
+
叶子判定:SCOPE 且 children 已空(直接子全 ENDED 被 detach):
|
|
2349
|
+
- 停在 ParallelGateway(fork 停驻,分支直通 end、无 join 汇聚)-> 结束
|
|
2350
|
+
自身(root 结束 = 实例完成),逐层向上继续收;
|
|
2351
|
+
- 停在 SubProcess(内部全部走完)-> 复活:结算 subProcess actinst、
|
|
2352
|
+
恢复 TOKEN 沿 sub 出边推进,产生新到达事件交由调用方 pump;
|
|
2353
|
+
- 停在普通节点(如 join 汇聚后主线停在 userTask)-> 主线身份,不收
|
|
2354
|
+
(join 汇聚恢复时已把 role 复位 TOKEN,这里双重防御)。
|
|
2355
|
+
"""
|
|
2356
|
+
root = pi.root_execution
|
|
2357
|
+
if root is None or root.state == ExecutionState.ENDED:
|
|
2358
|
+
return []
|
|
2359
|
+
arrivals: List[_Arrival] = []
|
|
2360
|
+
changed = True
|
|
2361
|
+
while changed:
|
|
2362
|
+
changed = False
|
|
2363
|
+
for e in list(pi.executions.values()):
|
|
2364
|
+
if e.state != ExecutionState.ACTIVE or e.role != "SCOPE":
|
|
2365
|
+
continue
|
|
2366
|
+
if e.children:
|
|
2367
|
+
continue # 仍有活跃子树(含子流程内部执行中)
|
|
2368
|
+
node = (
|
|
2369
|
+
self._container_of(pi, e).flow_nodes.get(e.activity_id)
|
|
2370
|
+
if e.activity_id
|
|
2371
|
+
else None
|
|
2372
|
+
)
|
|
2373
|
+
if isinstance(node, SubProcess):
|
|
2374
|
+
# 子流程内部全部收束:结算 actinst / 边界 timer / 容器订阅
|
|
2375
|
+
self._close_activity(pi, e, node)
|
|
2376
|
+
self._drop_boundary_jobs(pi, node) # 正常离开:sub 边界 timer 作废
|
|
2377
|
+
# M4-2b3/M4-2d:该 sub 容器 esc 订阅一并失效(timer job +
|
|
2378
|
+
# message/signal 订阅;root 兼任时流程级订阅保留)
|
|
2379
|
+
self._drop_scope_event_jobs(pi, e, node.id)
|
|
2380
|
+
self._drop_scope_event_subscriptions(pi, e, node.id)
|
|
2381
|
+
if node.multi_instance is not None:
|
|
2382
|
+
# M4-2c3:subProcess 宿主多实例——e 是顺序容器自身或某并行
|
|
2383
|
+
# 实例载体,本实例内部流转已走完 -> 走实例完成路径(计数/
|
|
2384
|
+
# 续跑下一实例/条件/收束容器),推进事件并入 arrivals
|
|
2385
|
+
mi_scope = self._mi_scope_of(pi, e)
|
|
2386
|
+
if mi_scope is not None:
|
|
2387
|
+
arrivals.extend(
|
|
2388
|
+
self._complete_mi_instance(pi, e, node, mi_scope)
|
|
2389
|
+
)
|
|
2390
|
+
changed = True
|
|
2391
|
+
continue
|
|
2392
|
+
e.role = "TOKEN" # 恢复主线身份(后续 fork 会再置回 SCOPE)
|
|
2393
|
+
self._leave(pi, e, node, arrivals)
|
|
2394
|
+
changed = True
|
|
2395
|
+
elif isinstance(node, ParallelGateway):
|
|
2396
|
+
# fork 停驻且分支全结束(无 join 汇聚路径)-> 结束自身向上收
|
|
2397
|
+
e.state = ExecutionState.ENDED
|
|
2398
|
+
self._detach_from_parent(pi, e)
|
|
2399
|
+
if e.is_root or pi.root_execution is e:
|
|
2400
|
+
self._complete_instance(pi)
|
|
2401
|
+
changed = True
|
|
2402
|
+
# M4-2b:root 被流程级事件子流程接管后(中断清空 activity/子树),事件
|
|
2403
|
+
# 子流程 scope 收束完毕 -> 宿主 scope(流程实例)结束 = 实例完成。
|
|
2404
|
+
# root 先置 ENDED:收尾段路径下主线早已到 end(root 转停驻 SCOPE),
|
|
2405
|
+
# 收束完成即实例结束,避免 ACTIVE root 残行被 _build_snap 写回 RU。
|
|
2406
|
+
root = pi.root_execution
|
|
2407
|
+
if (
|
|
2408
|
+
root is not None
|
|
2409
|
+
and root.state == ExecutionState.ACTIVE
|
|
2410
|
+
and not root.children
|
|
2411
|
+
and root.activity_id is None
|
|
2412
|
+
and root.open_activity is None
|
|
2413
|
+
):
|
|
2414
|
+
root.state = ExecutionState.ENDED
|
|
2415
|
+
self._complete_instance(pi)
|
|
2416
|
+
return arrivals
|
|
2417
|
+
|
|
2418
|
+
def _complete_instance(self, pi: ProcessInstance) -> None:
|
|
2419
|
+
pi.state = ProcessInstanceState.COMPLETED
|
|
2420
|
+
pi.end_time = _now()
|
|
2421
|
+
# M4-2b3:实例完成 -> 该实例全部实例级作业作废(订阅/边界/catch 无残留)
|
|
2422
|
+
for jid in [
|
|
2423
|
+
j.id
|
|
2424
|
+
for j in self._jobs.values()
|
|
2425
|
+
if j.process_instance_id == pi.id
|
|
2426
|
+
]:
|
|
2427
|
+
self._jobs.pop(jid, None)
|
|
2428
|
+
|
|
2429
|
+
# ------------------------------------------------------------------
|
|
2430
|
+
# 活动实例历史
|
|
2431
|
+
# ------------------------------------------------------------------
|
|
2432
|
+
def _open_activity(self, pi: ProcessInstance, token: Execution, node: FlowNode) -> None:
|
|
2433
|
+
# asyncBefore 拆分时已 open(token.open_activity 未结算)-> 行为续跑复用,
|
|
2434
|
+
# 避免 async job 执行时重复记一条 actinst
|
|
2435
|
+
if (
|
|
2436
|
+
token.open_activity is not None
|
|
2437
|
+
and token.open_activity.activity_id == node.id
|
|
2438
|
+
and token.open_activity.end_time is None
|
|
2439
|
+
):
|
|
2440
|
+
return
|
|
2441
|
+
ai = ActivityInstance(
|
|
2442
|
+
id=self._idgen.next_id(),
|
|
2443
|
+
process_instance_id=pi.id,
|
|
2444
|
+
activity_id=node.id,
|
|
2445
|
+
activity_name=node.name,
|
|
2446
|
+
execution_id=token.id,
|
|
2447
|
+
start_time=_now(),
|
|
2448
|
+
)
|
|
2449
|
+
token.open_activity = ai
|
|
2450
|
+
pi.activity_history.append(ai)
|
|
2451
|
+
|
|
2452
|
+
def _close_activity(self, pi: ProcessInstance, token: Execution, node: FlowNode) -> None:
|
|
2453
|
+
ai = token.open_activity
|
|
2454
|
+
if ai is not None and ai.activity_id == node.id and ai.end_time is None:
|
|
2455
|
+
ai.end_time = _now()
|
|
2456
|
+
token.open_activity = None
|
|
2457
|
+
|
|
2458
|
+
def _create_task(self, pi: ProcessInstance, token: Execution, node: UserTask) -> Task:
|
|
2459
|
+
task = Task(
|
|
2460
|
+
id=self._idgen.next_id(),
|
|
2461
|
+
name=node.name,
|
|
2462
|
+
process_instance_id=pi.id,
|
|
2463
|
+
execution_id=token.id,
|
|
2464
|
+
task_definition_key=node.id,
|
|
2465
|
+
assignee=node.assignee,
|
|
2466
|
+
candidate_users=list(node.candidate_users),
|
|
2467
|
+
candidate_groups=list(node.candidate_groups),
|
|
2468
|
+
create_time=_now(),
|
|
2469
|
+
)
|
|
2470
|
+
self._tasks[task.id] = task
|
|
2471
|
+
return task
|
|
2472
|
+
|
|
2473
|
+
def _run_delegate(self, pi: ProcessInstance, token: Execution, node: ServiceTask) -> None:
|
|
2474
|
+
ref = node.implementation_ref
|
|
2475
|
+
if ref is None:
|
|
2476
|
+
return # 无实现的 serviceTask 视为 pass-through
|
|
2477
|
+
fn = self._delegates.get(ref)
|
|
2478
|
+
if fn is None:
|
|
2479
|
+
raise ProcessInstanceException(
|
|
2480
|
+
f"serviceTask {node.id!r} 引用未注册的 delegate: {ref!r}"
|
|
2481
|
+
)
|
|
2482
|
+
result = fn(pi.variables)
|
|
2483
|
+
if isinstance(result, dict):
|
|
2484
|
+
pi.variables.update(result)
|
|
2485
|
+
|
|
2486
|
+
# ------------------------------------------------------------------
|
|
2487
|
+
# 静态出/入边辅助
|
|
2488
|
+
# ------------------------------------------------------------------
|
|
2489
|
+
@staticmethod
|
|
2490
|
+
def _outgoing(proc: Process, node: FlowNode) -> List[SequenceFlow]:
|
|
2491
|
+
return [proc.sequence_flows[fid] for fid in node.outgoing]
|
|
2492
|
+
|
|
2493
|
+
@staticmethod
|
|
2494
|
+
def _incoming(proc: Process, node: FlowNode) -> List[SequenceFlow]:
|
|
2495
|
+
return [proc.sequence_flows[fid] for fid in node.incoming]
|
|
2496
|
+
|
|
2497
|
+
# ------------------------------------------------------------------
|
|
2498
|
+
# M2/M3:持久化快照 / 崩溃恢复
|
|
2499
|
+
# ------------------------------------------------------------------
|
|
2500
|
+
def _build_snap(self, pi: ProcessInstance) -> "ProcInstSnap":
|
|
2501
|
+
"""把实例内存态转为落库快照(只含 ACTIVE execution / 活跃任务 / 实例级 job)。"""
|
|
2502
|
+
from camunda.persistence.store import (
|
|
2503
|
+
ActivitySnap,
|
|
2504
|
+
ExecutionSnap,
|
|
2505
|
+
JobSnap,
|
|
2506
|
+
ProcInstSnap,
|
|
2507
|
+
TaskSnap,
|
|
2508
|
+
)
|
|
2509
|
+
|
|
2510
|
+
# 按树序输出 execution(ACTIVE 才落 RU)
|
|
2511
|
+
active: List[Execution] = []
|
|
2512
|
+
stack = [pi.root_execution] if pi.root_execution else []
|
|
2513
|
+
while stack:
|
|
2514
|
+
e = stack.pop()
|
|
2515
|
+
if e is None:
|
|
2516
|
+
continue
|
|
2517
|
+
if e.state == ExecutionState.ACTIVE:
|
|
2518
|
+
active.append(e)
|
|
2519
|
+
stack.extend(reversed(e.children)) # 保持父->子顺序稳定
|
|
2520
|
+
|
|
2521
|
+
active_tasks = [t for t in self._tasks.values() if t.process_instance_id == pi.id]
|
|
2522
|
+
pi_jobs = [j for j in self._jobs.values() if j.process_instance_id == pi.id]
|
|
2523
|
+
|
|
2524
|
+
return ProcInstSnap(
|
|
2525
|
+
id=pi.id,
|
|
2526
|
+
process_definition_key=pi.process_definition_key,
|
|
2527
|
+
business_key=pi.business_key,
|
|
2528
|
+
state=pi.state.value,
|
|
2529
|
+
start_time=pi.start_time or "",
|
|
2530
|
+
end_time=pi.end_time,
|
|
2531
|
+
variables=dict(pi.variables),
|
|
2532
|
+
executions=[
|
|
2533
|
+
ExecutionSnap(
|
|
2534
|
+
id=e.id,
|
|
2535
|
+
parent_id=e.parent_id,
|
|
2536
|
+
activity_id=e.activity_id,
|
|
2537
|
+
role=e.role,
|
|
2538
|
+
mi=e.mi, # M4-2c4:MI 容器/实例状态随 execution 落库
|
|
2539
|
+
)
|
|
2540
|
+
for e in active
|
|
2541
|
+
],
|
|
2542
|
+
tasks=[
|
|
2543
|
+
TaskSnap(
|
|
2544
|
+
id=t.id,
|
|
2545
|
+
name=t.name,
|
|
2546
|
+
execution_id=t.execution_id,
|
|
2547
|
+
task_definition_key=t.task_definition_key,
|
|
2548
|
+
assignee=t.assignee,
|
|
2549
|
+
create_time=t.create_time or "",
|
|
2550
|
+
end_time=None,
|
|
2551
|
+
)
|
|
2552
|
+
for t in active_tasks
|
|
2553
|
+
],
|
|
2554
|
+
jobs=[
|
|
2555
|
+
JobSnap(
|
|
2556
|
+
id=j.id,
|
|
2557
|
+
job_type=j.job_type,
|
|
2558
|
+
execution_id=j.execution_id,
|
|
2559
|
+
node_id=j.node_id,
|
|
2560
|
+
duedate=j.duedate,
|
|
2561
|
+
created=j.created,
|
|
2562
|
+
retries=j.retries,
|
|
2563
|
+
repeat=j.repeat,
|
|
2564
|
+
)
|
|
2565
|
+
for j in pi_jobs
|
|
2566
|
+
],
|
|
2567
|
+
activity_history=[
|
|
2568
|
+
ActivitySnap(
|
|
2569
|
+
id=a.id,
|
|
2570
|
+
activity_id=a.activity_id,
|
|
2571
|
+
activity_name=a.activity_name,
|
|
2572
|
+
execution_id=a.execution_id,
|
|
2573
|
+
start_time=a.start_time,
|
|
2574
|
+
end_time=a.end_time,
|
|
2575
|
+
)
|
|
2576
|
+
for a in pi.activity_history
|
|
2577
|
+
],
|
|
2578
|
+
completed_tasks=[
|
|
2579
|
+
TaskSnap(
|
|
2580
|
+
id=t.id,
|
|
2581
|
+
name=t.name,
|
|
2582
|
+
execution_id=t.execution_id,
|
|
2583
|
+
task_definition_key=t.task_definition_key,
|
|
2584
|
+
assignee=t.assignee,
|
|
2585
|
+
create_time=t.create_time or "",
|
|
2586
|
+
end_time=t.end_time,
|
|
2587
|
+
)
|
|
2588
|
+
for t in pi.completed_tasks
|
|
2589
|
+
],
|
|
2590
|
+
)
|
|
2591
|
+
|
|
2592
|
+
@classmethod
|
|
2593
|
+
def from_database(cls, url: str) -> "ProcessEngine":
|
|
2594
|
+
"""从数据库恢复引擎:加载部署定义 + 所有运行中实例。
|
|
2595
|
+
|
|
2596
|
+
相当于 Camunda 重启后 JobExecutor/引擎重新挂载 ACT_RU_* 状态。
|
|
2597
|
+
注意:恢复后需要自行 register_delegate 同名实现(bean 注册不落库)。
|
|
2598
|
+
"""
|
|
2599
|
+
from camunda.persistence.store import Store
|
|
2600
|
+
from camunda.parser.bpmn_parser import parse_bpmn_xml
|
|
2601
|
+
|
|
2602
|
+
store = Store(url)
|
|
2603
|
+
engine = cls(store=store)
|
|
2604
|
+
|
|
2605
|
+
# 1) 恢复部署:每 key 取最新版本重解析
|
|
2606
|
+
latest: Dict[str, dict] = {}
|
|
2607
|
+
for d in store.load_proc_defs():
|
|
2608
|
+
cur = latest.get(d["key"])
|
|
2609
|
+
if cur is None or d["version"] > cur["version"]:
|
|
2610
|
+
latest[d["key"]] = d
|
|
2611
|
+
for d in latest.values():
|
|
2612
|
+
xml = d["xml"]
|
|
2613
|
+
if not xml:
|
|
2614
|
+
continue
|
|
2615
|
+
model = parse_bpmn_xml(xml, source_name=d["key"])
|
|
2616
|
+
for proc in model.processes:
|
|
2617
|
+
engine._definitions[proc.id] = proc
|
|
2618
|
+
engine._definition_versions[proc.id] = d["version"]
|
|
2619
|
+
|
|
2620
|
+
# 2) 恢复定义级作业(timer-start:PROC_INST_ID_ IS NULL)
|
|
2621
|
+
for j in store.load_timer_start_jobs():
|
|
2622
|
+
if j.process_definition_key in engine._definitions:
|
|
2623
|
+
engine._jobs[j.id] = j
|
|
2624
|
+
|
|
2625
|
+
# 3) 恢复运行中实例(RU 表,含实例级 job)
|
|
2626
|
+
for snap in store.load_active_instances():
|
|
2627
|
+
engine._restore_instance(snap)
|
|
2628
|
+
return engine
|
|
2629
|
+
|
|
2630
|
+
def close(self) -> None:
|
|
2631
|
+
"""释放持久化连接(store 模式);纯内存引擎为无操作。
|
|
2632
|
+
|
|
2633
|
+
演示 / 测试在删除 db 文件前必须调用(Windows 下 SQLite 文件句柄
|
|
2634
|
+
由连接池持有,不释放则 unlink 报 PermissionError);关闭后引擎
|
|
2635
|
+
不应再执行任何命令。
|
|
2636
|
+
"""
|
|
2637
|
+
if self._store is not None:
|
|
2638
|
+
self._store.close()
|
|
2639
|
+
|
|
2640
|
+
def _restore_instance(self, snap: "ProcInstSnap") -> None:
|
|
2641
|
+
"""把库中活跃实例快照重建为内存运行时态。"""
|
|
2642
|
+
# 根 = parent_id 为空的 execution(RU 行父在前已保证插入顺序)
|
|
2643
|
+
by_id: Dict[str, Execution] = {}
|
|
2644
|
+
root: Optional[Execution] = None
|
|
2645
|
+
for ex in snap.executions:
|
|
2646
|
+
e = Execution(
|
|
2647
|
+
id=ex.id,
|
|
2648
|
+
process_instance_id=snap.id,
|
|
2649
|
+
parent_id=ex.parent_id,
|
|
2650
|
+
role=ex.role,
|
|
2651
|
+
activity_id=ex.activity_id,
|
|
2652
|
+
# M4-2c4:MI 容器/实例状态还原(浅拷贝防快照对象复用共享;
|
|
2653
|
+
# items 元素只读,容器计数/序号随实例完成在原位自增)
|
|
2654
|
+
mi=dict(ex.mi) if ex.mi else None,
|
|
2655
|
+
state=ExecutionState.ACTIVE,
|
|
2656
|
+
)
|
|
2657
|
+
by_id[e.id] = e
|
|
2658
|
+
if ex.parent_id is None:
|
|
2659
|
+
root = e
|
|
2660
|
+
for e in by_id.values():
|
|
2661
|
+
if e.parent_id and e.parent_id in by_id:
|
|
2662
|
+
by_id[e.parent_id].children.append(e)
|
|
2663
|
+
|
|
2664
|
+
pi = ProcessInstance(
|
|
2665
|
+
id=snap.id,
|
|
2666
|
+
process_definition_key=snap.process_definition_key,
|
|
2667
|
+
business_key=snap.business_key,
|
|
2668
|
+
state=ProcessInstanceState(snap.state),
|
|
2669
|
+
variables=dict(snap.variables),
|
|
2670
|
+
root_execution=root,
|
|
2671
|
+
start_time=snap.start_time,
|
|
2672
|
+
end_time=snap.end_time,
|
|
2673
|
+
executions=by_id,
|
|
2674
|
+
)
|
|
2675
|
+
# 活动历史还原(ACT_HI_ACTINST 快照)
|
|
2676
|
+
for a in snap.activity_history:
|
|
2677
|
+
ai = ActivityInstance(
|
|
2678
|
+
id=a.id,
|
|
2679
|
+
process_instance_id=snap.id,
|
|
2680
|
+
activity_id=a.activity_id,
|
|
2681
|
+
activity_name=a.activity_name,
|
|
2682
|
+
execution_id=a.execution_id,
|
|
2683
|
+
start_time=a.start_time,
|
|
2684
|
+
end_time=a.end_time,
|
|
2685
|
+
)
|
|
2686
|
+
pi.activity_history.append(ai)
|
|
2687
|
+
# 未结算的活动(如停在 userTask)挂回对应 execution,便于后续 close
|
|
2688
|
+
if a.end_time is None:
|
|
2689
|
+
owner = by_id.get(a.execution_id)
|
|
2690
|
+
if owner is not None:
|
|
2691
|
+
owner.open_activity = ai
|
|
2692
|
+
# join 等待状态还原:停在并行网关的 ACTIVE TOKEN 即 join 等待
|
|
2693
|
+
# (M4-2a:按各自所在容器解析节点类型,子流程内部 join 同样可还原)
|
|
2694
|
+
for e in by_id.values():
|
|
2695
|
+
if (
|
|
2696
|
+
e.state == ExecutionState.ACTIVE
|
|
2697
|
+
and e.activity_id
|
|
2698
|
+
and e.role != "SCOPE" # SCOPE 停驻 fork 网关不算 join 等待
|
|
2699
|
+
):
|
|
2700
|
+
node = self._container_of(pi, e).flow_nodes.get(e.activity_id)
|
|
2701
|
+
if isinstance(node, ParallelGateway):
|
|
2702
|
+
pi.register_join_arrival(e.activity_id, e.id)
|
|
2703
|
+
# 任务还原
|
|
2704
|
+
for t in snap.tasks:
|
|
2705
|
+
task = Task(
|
|
2706
|
+
id=t.id,
|
|
2707
|
+
name=t.name,
|
|
2708
|
+
process_instance_id=snap.id,
|
|
2709
|
+
execution_id=t.execution_id,
|
|
2710
|
+
task_definition_key=t.task_definition_key,
|
|
2711
|
+
assignee=t.assignee,
|
|
2712
|
+
create_time=t.create_time,
|
|
2713
|
+
)
|
|
2714
|
+
self._tasks[task.id] = task
|
|
2715
|
+
# 已归档任务还原(HI_TASKINST 带 end_time 跨重启保留;HI 全量重写语义下
|
|
2716
|
+
# 缺了它会让重启后的下一次 save 抹掉历史——见 M4-2b5 修复记录)
|
|
2717
|
+
for t in snap.completed_tasks:
|
|
2718
|
+
task = Task(
|
|
2719
|
+
id=t.id,
|
|
2720
|
+
name=t.name,
|
|
2721
|
+
process_instance_id=snap.id,
|
|
2722
|
+
execution_id=t.execution_id,
|
|
2723
|
+
task_definition_key=t.task_definition_key,
|
|
2724
|
+
assignee=t.assignee,
|
|
2725
|
+
create_time=t.create_time,
|
|
2726
|
+
end_time=t.end_time,
|
|
2727
|
+
)
|
|
2728
|
+
pi.completed_tasks.append(task)
|
|
2729
|
+
# 实例级作业还原(timer-catch / async-continuation 停等)
|
|
2730
|
+
for j in snap.jobs:
|
|
2731
|
+
self._jobs[j.id] = Job(
|
|
2732
|
+
id=j.id,
|
|
2733
|
+
job_type=j.job_type,
|
|
2734
|
+
duedate=j.duedate,
|
|
2735
|
+
created=j.created,
|
|
2736
|
+
process_instance_id=snap.id,
|
|
2737
|
+
execution_id=j.execution_id,
|
|
2738
|
+
node_id=j.node_id,
|
|
2739
|
+
retries=j.retries,
|
|
2740
|
+
repeat=j.repeat,
|
|
2741
|
+
)
|
|
2742
|
+
self._rebuild_event_subscriptions(pi)
|
|
2743
|
+
self._instances[snap.id] = pi
|
|
2744
|
+
|
|
2745
|
+
def _rebuild_event_subscriptions(self, pi: ProcessInstance) -> None:
|
|
2746
|
+
"""崩溃恢复:从执行树重推导消息/信号订阅(M4-2d4,纯内存派生态)。
|
|
2747
|
+
|
|
2748
|
+
订阅不落库(对齐 join 等待「恢复时从树推导」先例):按「停驻状态 =
|
|
2749
|
+
等待状态」重放注册,规则与运行期注册点一一对应:
|
|
2750
|
+
- root 未完成 -> 流程级容器 esc message/signal start 订阅(root 直通/
|
|
2751
|
+
兼任 sub 时与运行期一致双注册);
|
|
2752
|
+
- 停驻 SubProcess 的 SCOPE(actinst open)-> 该 sub 容器内 esc 订阅;
|
|
2753
|
+
embedded sub 宿主等待窗口 = 整段内部执行 -> 边界订阅同样重放
|
|
2754
|
+
(事件子流程 scope 运行期不注册边界,保持一致不重放);
|
|
2755
|
+
- 停在 message/signal catch 的 token -> catch 订阅;
|
|
2756
|
+
- 停在挂边界事件的宿主(userTask / asyncBefore 节点,actinst open)->
|
|
2757
|
+
边界订阅。timer 边界/esc job 已随 jobs 快照还原,幂等检查防止重放
|
|
2758
|
+
时重复注册 timer(只补 message/signal 订阅)。
|
|
2759
|
+
"""
|
|
2760
|
+
if pi.is_completed or pi.root_execution is None:
|
|
2761
|
+
return
|
|
2762
|
+
root_proc = self._definitions[pi.process_definition_key]
|
|
2763
|
+
self._register_event_subprocess_subscriptions(
|
|
2764
|
+
pi, pi.root_execution, root_proc, None
|
|
2765
|
+
)
|
|
2766
|
+
for e in list(pi.executions.values()):
|
|
2767
|
+
if e.state != ExecutionState.ACTIVE or not e.activity_id:
|
|
2768
|
+
continue
|
|
2769
|
+
waiting = e.open_activity is not None and e.open_activity.end_time is None
|
|
2770
|
+
if not waiting:
|
|
2771
|
+
continue
|
|
2772
|
+
node = self._container_of(pi, e).flow_nodes.get(e.activity_id)
|
|
2773
|
+
if node is None:
|
|
2774
|
+
continue
|
|
2775
|
+
if e.role == "SCOPE" and isinstance(node, SubProcess):
|
|
2776
|
+
# 容器激活(嵌入/事件子流程 scope):重放容器内 esc 订阅;
|
|
2777
|
+
# 嵌入子流程宿主等待窗口 = 整段内部执行 -> 边界订阅同样重放
|
|
2778
|
+
self._register_event_subprocess_subscriptions(
|
|
2779
|
+
pi, e, node.process, node.id
|
|
2780
|
+
)
|
|
2781
|
+
if not node.triggered_by_event:
|
|
2782
|
+
self._register_boundary_jobs(pi, e, node)
|
|
2783
|
+
continue
|
|
2784
|
+
if e.role == "SCOPE":
|
|
2785
|
+
continue # fork 网关等其它 SCOPE 停驻无订阅
|
|
2786
|
+
if isinstance(node, IntermediateCatchEvent):
|
|
2787
|
+
if node.message_name is not None or node.signal_name is not None:
|
|
2788
|
+
sub = EventSubscription(
|
|
2789
|
+
id=self._idgen.next_id(),
|
|
2790
|
+
kind="message" if node.message_name is not None else "signal",
|
|
2791
|
+
event_name=node.message_name or node.signal_name,
|
|
2792
|
+
process_instance_id=pi.id,
|
|
2793
|
+
execution_id=e.id,
|
|
2794
|
+
activity_id=None,
|
|
2795
|
+
node_id=node.id,
|
|
2796
|
+
node_kind="catch",
|
|
2797
|
+
is_interrupting=True,
|
|
2798
|
+
created=_now(),
|
|
2799
|
+
)
|
|
2800
|
+
self._event_subs[sub.id] = sub
|
|
2801
|
+
continue
|
|
2802
|
+
if isinstance(node, UserTask) or node.async_before:
|
|
2803
|
+
# 宿主等待活动:重放边界订阅(timer job 已还原,幂等跳过)
|
|
2804
|
+
self._register_boundary_jobs(pi, e, node)
|
|
2805
|
+
|
|
2806
|
+
# ------------------------------------------------------------------
|
|
2807
|
+
# M3:定义级 timer-start 作业(不挂实例,随部署生命周期)
|
|
2808
|
+
# ------------------------------------------------------------------
|
|
2809
|
+
def _make_timer_start_job(self, proc_key: str, start: StartEvent) -> Job:
|
|
2810
|
+
"""按 timer kind 构造 timer-start 作业,duedate = 首次触发时刻。
|
|
2811
|
+
|
|
2812
|
+
- date : 绝对触发时间点(parse_trigger_date 归一化本地时区)
|
|
2813
|
+
- duration: 相对部署时刻的一次性延迟
|
|
2814
|
+
- cycle : 周期重复。value 为 ISO R[n]/dur 时 repeat={"kind":"interval"};
|
|
2815
|
+
否则视为 quartz/cron 表达式(croniter 求值)。无限续排
|
|
2816
|
+
"""
|
|
2817
|
+
timer = start.timer
|
|
2818
|
+
now = _now()
|
|
2819
|
+
if timer.kind == "date":
|
|
2820
|
+
duedate = parse_trigger_date(timer.value)
|
|
2821
|
+
repeat = None
|
|
2822
|
+
elif timer.kind == "duration":
|
|
2823
|
+
duedate = format_iso(
|
|
2824
|
+
parse_iso(now) + timedelta(seconds=timer.delay_seconds or 0)
|
|
2825
|
+
)
|
|
2826
|
+
repeat = None
|
|
2827
|
+
else: # cycle
|
|
2828
|
+
repeat = parse_iso_repeat(timer.value)
|
|
2829
|
+
if repeat is None: # 非 ISO 重复 => quartz/cron 表达式
|
|
2830
|
+
repeat = {"kind": "cron", "expr": timer.value.strip()}
|
|
2831
|
+
duedate = format_iso(next_trigger(repeat, parse_iso(now)))
|
|
2832
|
+
return Job(
|
|
2833
|
+
id=self._idgen.next_id(),
|
|
2834
|
+
job_type="timer-start",
|
|
2835
|
+
duedate=duedate,
|
|
2836
|
+
created=now,
|
|
2837
|
+
process_definition_key=proc_key,
|
|
2838
|
+
node_id=start.id,
|
|
2839
|
+
repeat=repeat,
|
|
2840
|
+
)
|
|
2841
|
+
|
|
2842
|
+
def _drop_definition_jobs(self, proc_key: str) -> None:
|
|
2843
|
+
"""移除某 process key 的全部定义级作业(重部署新版本前调用)。"""
|
|
2844
|
+
stale = [
|
|
2845
|
+
j.id
|
|
2846
|
+
for j in self._jobs.values()
|
|
2847
|
+
if j.is_definition_level and j.process_definition_key == proc_key
|
|
2848
|
+
]
|
|
2849
|
+
for jid in stale:
|
|
2850
|
+
del self._jobs[jid]
|
|
2851
|
+
|
|
2852
|
+
def _definition_level_jobs(self) -> List[Job]:
|
|
2853
|
+
"""当前全部定义级作业(timer-start 组)。"""
|
|
2854
|
+
return [j for j in self._jobs.values() if j.is_definition_level]
|
|
2855
|
+
|
|
2856
|
+
def _sync_timer_start_jobs(self) -> None:
|
|
2857
|
+
"""store 模式:定义级作业组全量落库(部署 / 触发续排 / 删除时调用)。"""
|
|
2858
|
+
if self._store is not None:
|
|
2859
|
+
self._store.save_timer_start_jobs(self._definition_level_jobs())
|
|
2860
|
+
|
|
2861
|
+
# ------------------------------------------------------------------
|
|
2862
|
+
# M3:JobExecutor 引擎侧(execute_due_jobs 即 Camunda JobExecutor 轮询)
|
|
2863
|
+
# ------------------------------------------------------------------
|
|
2864
|
+
def execute_due_jobs(
|
|
2865
|
+
self,
|
|
2866
|
+
limit: Optional[int] = None,
|
|
2867
|
+
*,
|
|
2868
|
+
lock_owner: Optional[str] = None,
|
|
2869
|
+
lease_seconds: int = 300,
|
|
2870
|
+
) -> int:
|
|
2871
|
+
"""执行当前到期且非死信的作业,返回执行条数。
|
|
2872
|
+
|
|
2873
|
+
JobExecutor 轮询线程与手动触发都调本方法(内部持引擎锁,与用户命令
|
|
2874
|
+
互斥)。续排出的新作业不在本次快照内,留待下一轮询 tick —— 周期作业
|
|
2875
|
+
不补触发(错过即错过,interval 按计划链续排不漂移)。
|
|
2876
|
+
|
|
2877
|
+
lock_owner is not None 且 self._store 不为空时(M7):走 DB CAS lease
|
|
2878
|
+
抢锁路径,多 JobExecutor / 多进程场景下保证同一作业只被一个节点执行。
|
|
2879
|
+
否则走原内存路径(单进程兼容)。
|
|
2880
|
+
"""
|
|
2881
|
+
if lock_owner is not None and self._store is not None:
|
|
2882
|
+
return self._execute_due_jobs_db(
|
|
2883
|
+
lock_owner, lease_seconds, limit if limit is not None else 50
|
|
2884
|
+
)
|
|
2885
|
+
with self._lock:
|
|
2886
|
+
due = sorted(
|
|
2887
|
+
(
|
|
2888
|
+
j
|
|
2889
|
+
for j in self._jobs.values()
|
|
2890
|
+
if j.is_due(_now()) and not j.is_dead()
|
|
2891
|
+
),
|
|
2892
|
+
key=lambda j: j.duedate,
|
|
2893
|
+
)
|
|
2894
|
+
if limit is not None:
|
|
2895
|
+
due = due[:limit]
|
|
2896
|
+
executed = 0
|
|
2897
|
+
for job in due:
|
|
2898
|
+
if self._jobs.get(job.id) is None:
|
|
2899
|
+
continue # 前序作业执行已连带删除(防御)
|
|
2900
|
+
try:
|
|
2901
|
+
self._execute_job(self._jobs[job.id])
|
|
2902
|
+
except Exception: # pragma: no cover - _execute_job 内部已兜底
|
|
2903
|
+
logger.exception("job %s 执行出现未预期异常", job.id)
|
|
2904
|
+
executed += 1
|
|
2905
|
+
return executed
|
|
2906
|
+
|
|
2907
|
+
def _execute_due_jobs_db(
|
|
2908
|
+
self, lock_owner: str, lease_seconds: int, batch_size: int
|
|
2909
|
+
) -> int:
|
|
2910
|
+
"""DB 抢锁路径(M7):用 store.acquire_due_jobs 拿到一批 due job,
|
|
2911
|
+
对每个 CAS 抢到的作业复用内存 _run_job_body 执行,结果用
|
|
2912
|
+
complete_job_cas / reschedule_job_cas 写回(不调 _persist_job_state
|
|
2913
|
+
全量重写,避免与并发 JobExecutor 争 LOCK 列)。
|
|
2914
|
+
|
|
2915
|
+
防御要点:
|
|
2916
|
+
- 内存里没有该 job(被别的节点推进 / 删了)-> 直接 CAS complete
|
|
2917
|
+
- CAS 写回失败(owner 已变更 = lease 过期被抢)-> 跳过(防御),
|
|
2918
|
+
但内存里的 _reschedule_or_remove / _degrade_after_failure 已改了
|
|
2919
|
+
mem_job,需要再次 save_proc_inst 落库让 DB 与内存一致
|
|
2920
|
+
- 失败回滚会重建内存 job(LOCK 列从 DB 读回)-> 用函数参数 owner
|
|
2921
|
+
而非 mem_job.lock_owner 做 CAS,保证身份一致
|
|
2922
|
+
"""
|
|
2923
|
+
acquired = self._store.acquire_due_jobs(
|
|
2924
|
+
lock_owner, lease_seconds, _now(), batch_size
|
|
2925
|
+
)
|
|
2926
|
+
if not acquired:
|
|
2927
|
+
return 0
|
|
2928
|
+
executed = 0
|
|
2929
|
+
for snap_job in acquired:
|
|
2930
|
+
mem_job = self._jobs.get(snap_job.id)
|
|
2931
|
+
if mem_job is None:
|
|
2932
|
+
# 内存没有(实例被别节点推进 / 删了),DB 行直接清掉
|
|
2933
|
+
self._store.complete_job_cas(snap_job.id, lock_owner)
|
|
2934
|
+
continue
|
|
2935
|
+
try:
|
|
2936
|
+
self._run_job_body(mem_job)
|
|
2937
|
+
except Exception:
|
|
2938
|
+
logger.warning(
|
|
2939
|
+
"db-locked job %s (%s@%s) 执行失败,剩余重试 %d",
|
|
2940
|
+
mem_job.id,
|
|
2941
|
+
mem_job.job_type,
|
|
2942
|
+
mem_job.node_id,
|
|
2943
|
+
mem_job.retries - 1,
|
|
2944
|
+
exc_info=True,
|
|
2945
|
+
)
|
|
2946
|
+
# 实例级失败:rollback 到上次同步点(DB LOCK 保留,内存重建后
|
|
2947
|
+
# lock_owner 是 None,CAS 必须用函数参数 owner)
|
|
2948
|
+
if mem_job.process_instance_id is not None:
|
|
2949
|
+
self._rollback_instance(mem_job.process_instance_id)
|
|
2950
|
+
mem_job = self._jobs.get(mem_job.id) or mem_job
|
|
2951
|
+
self._degrade_after_failure(mem_job)
|
|
2952
|
+
# CAS 写回(clear_lock=True:失败后让其他 JobExecutor 能看到
|
|
2953
|
+
# 新的 retries / duedate;duedate 已推到 retry_delay 之后,
|
|
2954
|
+
# 不会立刻被重抢)
|
|
2955
|
+
self._store.reschedule_job_cas(
|
|
2956
|
+
mem_job.id,
|
|
2957
|
+
lock_owner,
|
|
2958
|
+
mem_job.duedate,
|
|
2959
|
+
mem_job.retries,
|
|
2960
|
+
clear_lock=True,
|
|
2961
|
+
)
|
|
2962
|
+
# 实例级还要把实例快照全量落库(save_proc_inst 走原路径,
|
|
2963
|
+
# 不带 LOCK 列,与 CAS 写回的 LOCK 清掉语义吻合)
|
|
2964
|
+
if (
|
|
2965
|
+
mem_job.process_instance_id is not None
|
|
2966
|
+
and self._instances.get(mem_job.process_instance_id) is not None
|
|
2967
|
+
):
|
|
2968
|
+
pi = self._instances[mem_job.process_instance_id]
|
|
2969
|
+
self._store.save_proc_inst(self._build_snap(pi))
|
|
2970
|
+
elif mem_job.is_definition_level:
|
|
2971
|
+
self._sync_timer_start_jobs()
|
|
2972
|
+
executed += 1
|
|
2973
|
+
continue
|
|
2974
|
+
# 成功:一次性 / 续排
|
|
2975
|
+
self._reschedule_or_remove(mem_job)
|
|
2976
|
+
if mem_job.id not in self._jobs:
|
|
2977
|
+
# 一次性作业:从 DB 删除(CAS 防御 owner 不匹配)
|
|
2978
|
+
self._store.complete_job_cas(mem_job.id, lock_owner)
|
|
2979
|
+
else:
|
|
2980
|
+
# 续排(timer-start cycle):CAS 更新 duedate + 清 LOCK
|
|
2981
|
+
self._store.reschedule_job_cas(
|
|
2982
|
+
mem_job.id,
|
|
2983
|
+
lock_owner,
|
|
2984
|
+
mem_job.duedate,
|
|
2985
|
+
mem_job.retries,
|
|
2986
|
+
clear_lock=True,
|
|
2987
|
+
)
|
|
2988
|
+
# 同步实例级状态(Execution / Task / Variable)到 DB:
|
|
2989
|
+
# 一次性作业执行时可能推进 token(创建/删除 execution、新增 task、
|
|
2990
|
+
# 改变量),必须 save_proc_inst 全量重写该实例的 RU 行,
|
|
2991
|
+
# 否则重启后这些变更丢失。timer-start 的实例未变,仍在 _definitions;
|
|
2992
|
+
# _sync_timer_start_jobs 由 reschedule_job_cas 已处理(清 LOCK)。
|
|
2993
|
+
if (
|
|
2994
|
+
mem_job.process_instance_id is not None
|
|
2995
|
+
and self._instances.get(mem_job.process_instance_id) is not None
|
|
2996
|
+
):
|
|
2997
|
+
pi = self._instances[mem_job.process_instance_id]
|
|
2998
|
+
self._store.save_proc_inst(self._build_snap(pi))
|
|
2999
|
+
elif mem_job.is_definition_level:
|
|
3000
|
+
# 防御:续例 + 续排路径下也再 sync 一次(reschedule_job_cas
|
|
3001
|
+
# 已写 ACT_RU_JOB 行,但 LOCK 列状态以那里为准)
|
|
3002
|
+
self._sync_timer_start_jobs()
|
|
3003
|
+
executed += 1
|
|
3004
|
+
return executed
|
|
3005
|
+
|
|
3006
|
+
def create_job_query(self, process_instance_id: Optional[str] = None) -> List[Job]:
|
|
3007
|
+
"""查看作业(对齐 createJobQuery,按 duedate 升序;死信 retries==0 可见)。"""
|
|
3008
|
+
with self._lock:
|
|
3009
|
+
jobs = list(self._jobs.values())
|
|
3010
|
+
if process_instance_id is not None:
|
|
3011
|
+
jobs = [j for j in jobs if j.process_instance_id == process_instance_id]
|
|
3012
|
+
return sorted(jobs, key=lambda j: j.duedate)
|
|
3013
|
+
|
|
3014
|
+
def delete_job(self, job_id: str) -> None:
|
|
3015
|
+
"""手动删除作业(对齐 ManagementService.deleteJob)。
|
|
3016
|
+
|
|
3017
|
+
注意:删除 timer-catch/async 作业后对应 token 将永久停驻 —— Camunda
|
|
3018
|
+
语义相同(删 job 即放弃该次调度),M3 不做额外护栏。
|
|
3019
|
+
"""
|
|
3020
|
+
with self._lock:
|
|
3021
|
+
job = self._jobs.get(job_id)
|
|
3022
|
+
if job is None:
|
|
3023
|
+
raise NotFoundException(f"作业不存在: {job_id!r}")
|
|
3024
|
+
del self._jobs[job_id]
|
|
3025
|
+
if self._store is not None:
|
|
3026
|
+
if job.is_definition_level:
|
|
3027
|
+
self._sync_timer_start_jobs()
|
|
3028
|
+
else:
|
|
3029
|
+
pi = self._instances.get(job.process_instance_id)
|
|
3030
|
+
if pi is not None:
|
|
3031
|
+
self._store.save_proc_inst(self._build_snap(pi))
|
|
3032
|
+
|
|
3033
|
+
# ------------------------------------------------------------------
|
|
3034
|
+
# M3:单条作业执行
|
|
3035
|
+
# ------------------------------------------------------------------
|
|
3036
|
+
def _execute_job(self, job: Job) -> None:
|
|
3037
|
+
"""执行一条作业:成功 -> 删除/续排 + 落库;失败 -> 重试降级 + 落库。
|
|
3038
|
+
|
|
3039
|
+
失败不向外抛(避免单条失败打断整轮轮询)。持久化模式实例级失败会先
|
|
3040
|
+
回滚到上次同步点再降级,避免「内存已推进、库未写」的半执行不一致。
|
|
3041
|
+
"""
|
|
3042
|
+
try:
|
|
3043
|
+
self._run_job_body(job)
|
|
3044
|
+
except Exception:
|
|
3045
|
+
logger.warning(
|
|
3046
|
+
"job %s (%s@%s) 执行失败,剩余重试 %d",
|
|
3047
|
+
job.id,
|
|
3048
|
+
job.job_type,
|
|
3049
|
+
job.node_id,
|
|
3050
|
+
job.retries - 1,
|
|
3051
|
+
exc_info=True,
|
|
3052
|
+
)
|
|
3053
|
+
if job.process_instance_id is not None and self._store is not None:
|
|
3054
|
+
# 实例级 + 持久化:整体回滚到上次同步点(RU/HI 未动,内存重建)
|
|
3055
|
+
self._rollback_instance(job.process_instance_id)
|
|
3056
|
+
job = self._jobs.get(job.id) or job # rollback 重建了 job 对象
|
|
3057
|
+
self._degrade_after_failure(job)
|
|
3058
|
+
if self._store is not None:
|
|
3059
|
+
self._persist_job_state(job)
|
|
3060
|
+
else:
|
|
3061
|
+
self._reschedule_or_remove(job)
|
|
3062
|
+
if self._store is not None:
|
|
3063
|
+
self._persist_job_state(job)
|
|
3064
|
+
|
|
3065
|
+
def _run_job_body(self, job: Job) -> None:
|
|
3066
|
+
"""纯执行单条作业(不落库)。失败向上抛,调用方决定降级 / 续排策略。
|
|
3067
|
+
|
|
3068
|
+
拆出来供 DB 抢锁路径(M7)复用:CAS 写回而非全量重写,避免与
|
|
3069
|
+
其他 JobExecutor 产生 LOCK 列竞态。
|
|
3070
|
+
"""
|
|
3071
|
+
if job.job_type == "timer-start":
|
|
3072
|
+
self._fire_timer_start(job)
|
|
3073
|
+
elif job.job_type == "timer-catch":
|
|
3074
|
+
self._fire_timer_catch(job)
|
|
3075
|
+
elif job.job_type == "timer-boundary":
|
|
3076
|
+
self._fire_timer_boundary(job)
|
|
3077
|
+
elif job.job_type == "timer-event-start":
|
|
3078
|
+
self._fire_timer_event_start(job)
|
|
3079
|
+
elif job.job_type == "async-continuation":
|
|
3080
|
+
self._run_async_continuation(job)
|
|
3081
|
+
elif job.job_type == "async-after":
|
|
3082
|
+
self._run_async_after(job)
|
|
3083
|
+
else:
|
|
3084
|
+
raise InvalidRequestException(f"未知作业类型: {job.job_type!r}")
|
|
3085
|
+
|
|
3086
|
+
def _fire_timer_start(self, job: Job) -> None:
|
|
3087
|
+
"""timer-start 触发:启动一个流程实例(cycle 续排由 _reschedule_or_remove 处理)。"""
|
|
3088
|
+
proc = self._definitions[job.process_definition_key]
|
|
3089
|
+
start = proc.flow_nodes[job.node_id]
|
|
3090
|
+
if not isinstance(start, StartEvent):
|
|
3091
|
+
raise ProcessInstanceException(
|
|
3092
|
+
f"timer-start job {job.id} 指向非 startEvent 节点: {job.node_id!r}"
|
|
3093
|
+
)
|
|
3094
|
+
# _start_process 内部已落库实例(store 模式);定时启动不带用户变量
|
|
3095
|
+
self._start_process(proc, None, None, start)
|
|
3096
|
+
|
|
3097
|
+
def _fire_timer_catch(self, job: Job) -> None:
|
|
3098
|
+
"""timer-catch 到期:结算停等 actinst,token 沿出边继续推进。"""
|
|
3099
|
+
pi = self._instances.get(job.process_instance_id)
|
|
3100
|
+
token = pi.executions.get(job.execution_id) if pi is not None else None
|
|
3101
|
+
if (
|
|
3102
|
+
pi is None
|
|
3103
|
+
or token is None
|
|
3104
|
+
or token.state != ExecutionState.ACTIVE
|
|
3105
|
+
or token.activity_id != job.node_id
|
|
3106
|
+
):
|
|
3107
|
+
self._jobs.pop(job.id, None) # token 已失效/推进 -> 过期作业直接丢弃
|
|
3108
|
+
return
|
|
3109
|
+
proc = self._container_of(pi, token)
|
|
3110
|
+
node = proc.flow_nodes[job.node_id]
|
|
3111
|
+
self._close_activity(pi, token, node)
|
|
3112
|
+
arrivals: List[_Arrival] = []
|
|
3113
|
+
self._leave(pi, token, node, arrivals)
|
|
3114
|
+
self._pump(pi, arrivals)
|
|
3115
|
+
|
|
3116
|
+
def _run_async_continuation(self, job: Job) -> None:
|
|
3117
|
+
"""async-continuation 到期:直接执行节点行为(_dispatch_node 不再拆 async)。
|
|
3118
|
+
|
|
3119
|
+
token 在 asyncBefore 拆分时已 open actinst 停等;job 执行 = 行为主体
|
|
3120
|
+
(open 复用避免重复记 actinst),完成后继续推进。
|
|
3121
|
+
"""
|
|
3122
|
+
pi = self._instances.get(job.process_instance_id)
|
|
3123
|
+
token = pi.executions.get(job.execution_id) if pi is not None else None
|
|
3124
|
+
if (
|
|
3125
|
+
pi is None
|
|
3126
|
+
or pi.is_completed
|
|
3127
|
+
or token is None
|
|
3128
|
+
or token.state != ExecutionState.ACTIVE
|
|
3129
|
+
):
|
|
3130
|
+
self._jobs.pop(job.id, None)
|
|
3131
|
+
return
|
|
3132
|
+
proc = self._container_of(pi, token)
|
|
3133
|
+
node = proc.flow_nodes[job.node_id]
|
|
3134
|
+
arrivals = self._dispatch_node(pi, token, node)
|
|
3135
|
+
self._pump(pi, arrivals)
|
|
3136
|
+
# 宿主(asyncBefore 节点)活动是否仍在等待:仅当行为后 token 仍停在同一
|
|
3137
|
+
# 节点且活动未结算(asyncBefore + userTask 组合停等)时边界 timer 继续
|
|
3138
|
+
# 有效;已离开 / 停在 join 等待(并行网关无活动等待窗口)即作废
|
|
3139
|
+
still_waiting = (
|
|
3140
|
+
token.state == ExecutionState.ACTIVE
|
|
3141
|
+
and token.activity_id == node.id
|
|
3142
|
+
and token.open_activity is not None
|
|
3143
|
+
and token.open_activity.end_time is None
|
|
3144
|
+
and token.id not in pi.join_arrived(node.id)
|
|
3145
|
+
)
|
|
3146
|
+
if not still_waiting:
|
|
3147
|
+
self._drop_boundary_jobs(pi, node)
|
|
3148
|
+
|
|
3149
|
+
def _run_async_after(self, job: Job) -> None:
|
|
3150
|
+
"""async-after 到期:执行「离开推进」。
|
|
3151
|
+
|
|
3152
|
+
serviceTask 沿出边离开(多出边 fork);XOR 此时重新求值条件选路后 take
|
|
3153
|
+
—— 行为与离开之间的异步窗口内变量可能已变化,条件以 job 到期时刻为准。
|
|
3154
|
+
token 已推进到别处(activity_id != job.node_id)= 过期作业,直接丢弃。
|
|
3155
|
+
"""
|
|
3156
|
+
pi = self._instances.get(job.process_instance_id)
|
|
3157
|
+
token = pi.executions.get(job.execution_id) if pi is not None else None
|
|
3158
|
+
if (
|
|
3159
|
+
pi is None
|
|
3160
|
+
or pi.is_completed
|
|
3161
|
+
or token is None
|
|
3162
|
+
or token.state != ExecutionState.ACTIVE
|
|
3163
|
+
or token.activity_id != job.node_id
|
|
3164
|
+
):
|
|
3165
|
+
self._jobs.pop(job.id, None) # token 已推进 -> 过期作业丢弃
|
|
3166
|
+
return
|
|
3167
|
+
proc = self._container_of(pi, token)
|
|
3168
|
+
node = proc.flow_nodes[job.node_id]
|
|
3169
|
+
arrivals: List[_Arrival] = []
|
|
3170
|
+
if isinstance(node, ServiceTask):
|
|
3171
|
+
self._leave(pi, token, node, arrivals)
|
|
3172
|
+
elif isinstance(node, ExclusiveGateway):
|
|
3173
|
+
chosen = select_exclusive_gateway_flow(
|
|
3174
|
+
node, self._outgoing(proc, node), pi.variables
|
|
3175
|
+
)
|
|
3176
|
+
self._take(pi, token, chosen, arrivals)
|
|
3177
|
+
else: # 防御:异常宿主类型 -> 丢弃(正常解析 + _handle_arrival 校验后不会发生)
|
|
3178
|
+
self._jobs.pop(job.id, None)
|
|
3179
|
+
return
|
|
3180
|
+
self._pump(pi, arrivals)
|
|
3181
|
+
|
|
3182
|
+
def _reschedule_or_remove(self, job: Job) -> None:
|
|
3183
|
+
"""作业成功后处理:timer-start 按 repeat 续排下一 duedate;其余删除。
|
|
3184
|
+
|
|
3185
|
+
interval 按「计划 duedate 链式 + 周期」续排(执行延迟不累积漂移);
|
|
3186
|
+
cron 从当前时刻求下一未来触发。count 递减到 0 即停排(R3/PT.. = 触发 3 次)。
|
|
3187
|
+
"""
|
|
3188
|
+
if not job.repeat:
|
|
3189
|
+
self._jobs.pop(job.id, None)
|
|
3190
|
+
return
|
|
3191
|
+
rep = dict(job.repeat)
|
|
3192
|
+
if rep["kind"] == "interval":
|
|
3193
|
+
if rep.get("count") is not None:
|
|
3194
|
+
rep["count"] -= 1
|
|
3195
|
+
if rep["count"] <= 0:
|
|
3196
|
+
self._jobs.pop(job.id, None)
|
|
3197
|
+
return
|
|
3198
|
+
job.repeat = rep
|
|
3199
|
+
job.duedate = format_iso(
|
|
3200
|
+
parse_iso(job.duedate) + timedelta(seconds=rep["seconds"])
|
|
3201
|
+
)
|
|
3202
|
+
else: # cron
|
|
3203
|
+
job.duedate = format_iso(next_trigger(rep, parse_iso(_now())))
|
|
3204
|
+
job.repeat = rep
|
|
3205
|
+
|
|
3206
|
+
def _degrade_after_failure(self, job: Job) -> None:
|
|
3207
|
+
"""失败降级:retries-1;未耗尽则按 retry_delay_seconds 顺延 duedate。"""
|
|
3208
|
+
job.retries -= 1
|
|
3209
|
+
if job.retries > 0:
|
|
3210
|
+
job.duedate = format_iso(
|
|
3211
|
+
parse_iso(_now()) + timedelta(seconds=job.retry_delay_seconds)
|
|
3212
|
+
)
|
|
3213
|
+
# retries 耗尽 = 死信:保留记录(可 create_job_query 查看),不再被 acquire
|
|
3214
|
+
|
|
3215
|
+
def _rollback_instance(self, proc_inst_id: str) -> None:
|
|
3216
|
+
"""把实例内存态回滚到上次同步点(从 RU/HI 重读重建,store 未动无需回写)。"""
|
|
3217
|
+
snap = next(
|
|
3218
|
+
(s for s in self._store.load_active_instances() if s.id == proc_inst_id),
|
|
3219
|
+
None,
|
|
3220
|
+
)
|
|
3221
|
+
if snap is None:
|
|
3222
|
+
return
|
|
3223
|
+
for tid in [t.id for t in self._tasks.values() if t.process_instance_id == proc_inst_id]:
|
|
3224
|
+
self._tasks.pop(tid, None)
|
|
3225
|
+
for jid in [j.id for j in self._jobs.values() if j.process_instance_id == proc_inst_id]:
|
|
3226
|
+
self._jobs.pop(jid, None)
|
|
3227
|
+
self._instances.pop(proc_inst_id, None)
|
|
3228
|
+
self._restore_instance(snap)
|
|
3229
|
+
|
|
3230
|
+
def _persist_job_state(self, job: Job) -> None:
|
|
3231
|
+
"""作业执行成功/失败后的事务边界同步(store 模式)。"""
|
|
3232
|
+
if job.is_definition_level:
|
|
3233
|
+
self._sync_timer_start_jobs()
|
|
3234
|
+
return
|
|
3235
|
+
pi = self._instances.get(job.process_instance_id)
|
|
3236
|
+
if pi is not None:
|
|
3237
|
+
self._store.save_proc_inst(self._build_snap(pi))
|