tasklite-engine 1.0.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.
- tasklite/__init__.py +55 -0
- tasklite/backend/__init__.py +1 -0
- tasklite/backend/base.py +199 -0
- tasklite/backend/sqlite_backend.py +570 -0
- tasklite/contrib/__init__.py +6 -0
- tasklite/engine/__init__.py +16 -0
- tasklite/engine/completion.py +439 -0
- tasklite/engine/deadlock.py +33 -0
- tasklite/engine/dispatch.py +426 -0
- tasklite/engine/executor.py +1202 -0
- tasklite/engine/failure.py +509 -0
- tasklite/engine/inflight.py +25 -0
- tasklite/engine/loop.py +281 -0
- tasklite/engine/recovery.py +409 -0
- tasklite/engine/resource.py +240 -0
- tasklite/engine/retry.py +147 -0
- tasklite/engine/runtime.py +331 -0
- tasklite/engine/scheduler.py +346 -0
- tasklite/error_codes.py +66 -0
- tasklite/exceptions.py +215 -0
- tasklite/models/__init__.py +6 -0
- tasklite/models/context.py +313 -0
- tasklite/models/job.py +304 -0
- tasklite/models/state.py +413 -0
- tasklite/pipeline.py +914 -0
- tasklite/pipeline_util.py +204 -0
- tasklite/py.typed +0 -0
- tasklite/utils/__init__.py +6 -0
- tasklite/utils/ipc.py +88 -0
- tasklite/utils/jsonutil.py +65 -0
- tasklite/utils/lockfile.py +154 -0
- tasklite/utils/validation.py +177 -0
- tasklite/wrappers/__init__.py +26 -0
- tasklite/wrappers/discovery.py +620 -0
- tasklite_engine-1.0.0.dist-info/METADATA +321 -0
- tasklite_engine-1.0.0.dist-info/RECORD +39 -0
- tasklite_engine-1.0.0.dist-info/WHEEL +5 -0
- tasklite_engine-1.0.0.dist-info/licenses/LICENSE +21 -0
- tasklite_engine-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
"""派发机器:派发预检与 submit 编排。
|
|
2
|
+
|
|
3
|
+
五关顺序即契约(顺序即时序约束):dedup → dep-failed → no-handler →
|
|
4
|
+
orphan-probe → stale-restore。依赖经 RunContext(``self._ctx``)注入,
|
|
5
|
+
经 ``self._failure``/``self._completion`` 复用失败机器与完成机器,
|
|
6
|
+
不反向引用 TaskLite。
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
import random
|
|
11
|
+
import time
|
|
12
|
+
import traceback
|
|
13
|
+
from typing import List, Optional, Tuple, TYPE_CHECKING
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from .runtime import RunContext
|
|
17
|
+
from .failure import FailureMachine
|
|
18
|
+
from .completion import CompletionMachine
|
|
19
|
+
|
|
20
|
+
from ..error_codes import (
|
|
21
|
+
ERR_DISPATCH_FAILURE as _ERR_DISPATCH_FAILURE,
|
|
22
|
+
ERR_JOB_DEPENDENCY as _ERR_JOB_DEPENDENCY,
|
|
23
|
+
ERR_NO_HANDLER as _ERR_NO_HANDLER,
|
|
24
|
+
ERR_PAYLOAD_VALIDATION as _ERR_PAYLOAD_VALIDATION,
|
|
25
|
+
)
|
|
26
|
+
from ..exceptions import _CommitCrashSignal, _JobTerminated
|
|
27
|
+
from ..models.context import TaskContext
|
|
28
|
+
from ..models.job import Job
|
|
29
|
+
from .runtime import RT_BACKOFF_UNTIL, RT_BACKOFF_WALL_DEADLINE
|
|
30
|
+
from .executor import JobHandle
|
|
31
|
+
from .inflight import InFlightJob
|
|
32
|
+
from .retry import rerun_skips
|
|
33
|
+
from ..utils.ipc import inputs_path, outputs_path, signals_path
|
|
34
|
+
from ..utils.lockfile import probe_lock
|
|
35
|
+
from ..utils.validation import validate_payload
|
|
36
|
+
|
|
37
|
+
logger = logging.getLogger("tasklite")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class DispatchMachine:
|
|
41
|
+
"""派发预检 + 资源 acquire + 子进程 submit 的编排器。"""
|
|
42
|
+
|
|
43
|
+
def __init__(
|
|
44
|
+
self, ctx: "RunContext", failure: "FailureMachine", completion: "CompletionMachine"
|
|
45
|
+
) -> None:
|
|
46
|
+
self._ctx = ctx
|
|
47
|
+
self._failure = failure
|
|
48
|
+
self._completion = completion
|
|
49
|
+
|
|
50
|
+
def _reject_and_commit(
|
|
51
|
+
self, uid: str, job_dict: dict, meta: dict, *,
|
|
52
|
+
count_as: str = "failed",
|
|
53
|
+
) -> None:
|
|
54
|
+
"""拒绝 job 的统一出口:commit 失败 → 三连收敛 → 级联 → 钩子。
|
|
55
|
+
|
|
56
|
+
三处拒绝路径(依赖失败 / no-handler / payload 校验失败)共用本方法,
|
|
57
|
+
消除重复的 commit + apply_failed + cascade_fail + fire_hook 样板。
|
|
58
|
+
|
|
59
|
+
正常返回 = 处理完成(commit 成功或 3-strike DLQ 成功);
|
|
60
|
+
_CommitCrashSignal 穿透上抛 = 后端环境故障(由 run_loop 崩溃处理)。
|
|
61
|
+
"""
|
|
62
|
+
committed = self._ctx.backend.commit_job_failure(uid, meta)
|
|
63
|
+
if committed:
|
|
64
|
+
self._failure.apply_failed(uid, meta, count_as=count_as)
|
|
65
|
+
self._failure.cascade_fail(uid)
|
|
66
|
+
self._ctx.fire_job_completed(uid, meta, False, False)
|
|
67
|
+
return
|
|
68
|
+
# commit 失败 → 3-strike / 崩溃路径。commit_failed_crash 要么抛
|
|
69
|
+
# _JobTerminated(3-strike DLQ 成功,job 已终结),要么抛
|
|
70
|
+
# _CommitCrashSignal(后端环境故障,需崩溃重启)。
|
|
71
|
+
try:
|
|
72
|
+
self._failure.commit_failed_crash(
|
|
73
|
+
uid, meta.get("error", "unknown"), job_dict,
|
|
74
|
+
)
|
|
75
|
+
except _JobTerminated:
|
|
76
|
+
# 3-strike DLQ 成功——job 已终结,正常返回即可
|
|
77
|
+
return
|
|
78
|
+
# _CommitCrashSignal 继承 BaseException,不被上方 except 捕获,
|
|
79
|
+
# 自动穿透上抛到 run_loop 的崩溃处理分支。
|
|
80
|
+
|
|
81
|
+
def dispatch_dedup(self, state, uid: str, job_dict: dict) -> bool:
|
|
82
|
+
"""派发预检关 1——去重(is_known 命中 → rerun 策略 → skip/放行)。
|
|
83
|
+
|
|
84
|
+
返回 True = 已处理(job 被 skip 或放行后本关终结);返回 False
|
|
85
|
+
表示未命中(调用方继续后续预检关)。统一 is_known 谓词;
|
|
86
|
+
rerun 策略豁免 every_run/on_failure 的 wall/failed 命中。
|
|
87
|
+
"""
|
|
88
|
+
# Dedup check (job already completed or failed since queue was loaded)
|
|
89
|
+
# 统一 is_known 谓词。pop 自 queue 的 uid
|
|
90
|
+
# 不可能在 queue/in-flight(已出队)。命中时对后端做 commit_skip——
|
|
91
|
+
# 否则磁盘条目残留,每次 run 重复 pop→skip→drift。
|
|
92
|
+
# rerun 策略豁免——every_run/on_failure 任务命中
|
|
93
|
+
# wall/failed 时**放行重跑**(不 skip;磁盘残留由后续 commit 清理)。
|
|
94
|
+
if state.is_known(uid):
|
|
95
|
+
wall_hit = uid in state.wall
|
|
96
|
+
failed_hit = uid in state.failed
|
|
97
|
+
if rerun_skips(
|
|
98
|
+
job_dict, wall_hit=wall_hit, failed_hit=failed_hit,
|
|
99
|
+
wall_meta=state.wall.get(uid),
|
|
100
|
+
):
|
|
101
|
+
self._ctx.stats["skipped"] += 1
|
|
102
|
+
committed = self._ctx.backend.commit_skip(uid)
|
|
103
|
+
if not committed:
|
|
104
|
+
# commit_skip 终态模型:skip 命中 = uid 已有
|
|
105
|
+
# wall/failed 终态,此处只是清理磁盘残留。commit_skip 失败
|
|
106
|
+
# 是环境故障——走「requeue + 崩溃」契约,**绝不走 3-strike
|
|
107
|
+
# DLQ**(那会把 wall 成功记录翻转成失败)。下一次 run 的
|
|
108
|
+
# 加载期过滤或 commit_skip 重试消化残留。
|
|
109
|
+
self._failure.commit_skip_crash(uid, job_dict)
|
|
110
|
+
# 与 dep-failed/no-handler 分支同款:
|
|
111
|
+
# _commit_skip_crash 契约上永不正常返回;fall-through 仅当
|
|
112
|
+
# 契约被破坏时可达——fail-loud 优于静默「装作已处理」。
|
|
113
|
+
raise AssertionError(
|
|
114
|
+
"_commit_skip_crash unexpectedly returned normally"
|
|
115
|
+
)
|
|
116
|
+
else:
|
|
117
|
+
# pop_job 对 rerun 任务把 uid 加入
|
|
118
|
+
# _rerun_active_uids(豁免集合),skip 成功路径配对
|
|
119
|
+
# discard——否则豁免集合泄漏并永久弱化 DEBUG 互斥断言。
|
|
120
|
+
# 与 dep-failure/no-handler 直接 commit 路径的 discard
|
|
121
|
+
# 对称;对未注册 uid 是 no-op。
|
|
122
|
+
state.unregister_in_flight(uid)
|
|
123
|
+
return True
|
|
124
|
+
return False
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def dispatch_dep_failed(self, uid: str, job_dict: dict, pending_dep_failure: str) -> bool:
|
|
128
|
+
"""派发预检关 2——依赖失败(父任务已进 DLQ → 本任务 JOB_DEPENDENCY)。
|
|
129
|
+
|
|
130
|
+
返回 True = 已处理(依赖失败直接 commit,含级联下游 + 钩子);
|
|
131
|
+
仅当 pending_dep_failure 非空时调用。commit 失败走 3-strike / 崩溃路径。
|
|
132
|
+
"""
|
|
133
|
+
if pending_dep_failure is not None:
|
|
134
|
+
logger.warning(f"SKIP: {uid} (Dependency {pending_dep_failure} failed)")
|
|
135
|
+
fail_meta = {"error": _ERR_JOB_DEPENDENCY,
|
|
136
|
+
"failed_dependency": pending_dep_failure}
|
|
137
|
+
# 依赖父失败的级联下游计入 cascade_failed 而非 failed,
|
|
138
|
+
# 保证「真实业务失败率」统计不被级联稀释。
|
|
139
|
+
self._reject_and_commit(
|
|
140
|
+
uid, job_dict, fail_meta, count_as="cascade_failed",
|
|
141
|
+
)
|
|
142
|
+
return True
|
|
143
|
+
return False
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def dispatch_no_handler(self, uid: str, job_dict: dict, task_type: str) -> bool:
|
|
147
|
+
"""派发预检关 3——handler 未注册(NO_HANDLER 直接 DLQ)。
|
|
148
|
+
|
|
149
|
+
返回 True = 已处理;仅当 task_type 未注册时调用。
|
|
150
|
+
"""
|
|
151
|
+
if task_type not in self._ctx.handlers:
|
|
152
|
+
logger.error(f"No handler for: {task_type}")
|
|
153
|
+
self._reject_and_commit(
|
|
154
|
+
uid, job_dict, {"error": _ERR_NO_HANDLER},
|
|
155
|
+
)
|
|
156
|
+
return True
|
|
157
|
+
return False
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def dispatch_orphan_probe(self, state, uid: str, job_dict: dict) -> bool:
|
|
161
|
+
"""派发预检关 4——孤儿探测(probe 先于 restore)。
|
|
162
|
+
|
|
163
|
+
主进程仅探测(非阻塞试锁)。锁被占 = 同 uid 孤儿 worker 仍持锁
|
|
164
|
+
→ requeue + 短退避(上限 ~1s 防热循环)本轮不派发。
|
|
165
|
+
probe 必须**先于** _restore_stale_result 与残留声明清理——
|
|
166
|
+
孤儿存活时提前 return,绝不删孤儿实时声明。返回 True = 已处理
|
|
167
|
+
(孤儿存活 defer);否则调用方继续 restore/submit。
|
|
168
|
+
"""
|
|
169
|
+
# 主进程仅探测——非阻塞试锁,成功即释放。
|
|
170
|
+
# 锁生命周期 = 执行体生命周期:主进程崩溃不释放 worker 的锁,
|
|
171
|
+
# 探测失败 = 同 uid 孤儿 worker 仍持锁 → requeue + 短退避
|
|
172
|
+
# (上限 ~1s 防热循环)本轮 defer,下轮孤儿死后正常执行。
|
|
173
|
+
# probe 必须**先于** restore 与声明清理——
|
|
174
|
+
# 孤儿存活时提前 return,绝不删孤儿实时声明;probe 通过后
|
|
175
|
+
# restore 消费孤儿残留结果 → 不派发(无双跑)。
|
|
176
|
+
if not probe_lock(self._ctx.ipc_dir, uid):
|
|
177
|
+
logger.warning(
|
|
178
|
+
f"Deferring {uid}: orphan execution body still holds lock; "
|
|
179
|
+
f"requeue with short backoff."
|
|
180
|
+
)
|
|
181
|
+
self._ctx.stats["deferred_orphan"] += 1
|
|
182
|
+
# 与 retry 路径(_apply_result)对称写
|
|
183
|
+
# wall_deadline——只写 monotonic _backoff_until 的话,崩溃持久化后
|
|
184
|
+
# 重启 monotonic 归零 → 残留旧值被调度器误判为未来退避(阻塞数小时)。
|
|
185
|
+
# wall_deadline 由 _run_body 加载换算为 monotonic,跨崩溃安全。
|
|
186
|
+
# 抖动防热循环:compute_backoff(1,1,1) 恒 ∈ [0.75,1.25] 截顶到
|
|
187
|
+
# 1.0 即 [0.75,1.0] 常量抖动——与重试计数无关,直接写意图。
|
|
188
|
+
delay = random.uniform(0.75, 1.0)
|
|
189
|
+
rt = job_dict.setdefault("runtime", {})
|
|
190
|
+
rt[RT_BACKOFF_UNTIL] = time.monotonic() + delay
|
|
191
|
+
rt[RT_BACKOFF_WALL_DEADLINE] = time.time() + delay
|
|
192
|
+
state.requeue_jobs([job_dict], front=True)
|
|
193
|
+
return True
|
|
194
|
+
return False
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def dispatch_job(self, sched) -> Optional[InFlightJob]:
|
|
198
|
+
"""Pop job → 预检查 → acquire 资源 → submit 到子进程。直接操作 self._ctx.state。
|
|
199
|
+
|
|
200
|
+
返回 entry。entry 为 None 表示 job 走了「不走子进程」路径
|
|
201
|
+
(dedup/依赖失败/no-handler/payload 校验失败),已直接 commit 并返回。
|
|
202
|
+
entry 非 None 表示已 submit,需由 ``_complete_job`` 处理结果。
|
|
203
|
+
"""
|
|
204
|
+
state = self._ctx.state
|
|
205
|
+
runnable_idx = sched.runnable_idx
|
|
206
|
+
# kind 显式表达调度契约——"dep_failed" 表示 runnable_idx 指向
|
|
207
|
+
# dep-failed 兜底位置(pending_dep_failure 携带失败依赖),"runnable"
|
|
208
|
+
# 表示真正可运行的 job。消费方按 kind 走分支。
|
|
209
|
+
pending_dep_failure = sched.pending_dep_failure if sched.kind == "dep_failed" else None
|
|
210
|
+
|
|
211
|
+
job_dict = state.pop_job(runnable_idx)
|
|
212
|
+
job = Job.from_dict(job_dict)
|
|
213
|
+
uid = job.uid
|
|
214
|
+
# 预检五关以调用顺序表达时序契约,每关返回 True=已处理。
|
|
215
|
+
# 关 1-4(dedup/dep-failed/no-handler/orphan-probe)+ 关 5(stale-restore)。
|
|
216
|
+
if self.dispatch_dedup(state, uid, job_dict):
|
|
217
|
+
return None
|
|
218
|
+
if pending_dep_failure is not None:
|
|
219
|
+
if self.dispatch_dep_failed(uid, job_dict, pending_dep_failure):
|
|
220
|
+
return None
|
|
221
|
+
if job.task_type not in self._ctx.handlers:
|
|
222
|
+
if self.dispatch_no_handler(uid, job_dict, job.task_type):
|
|
223
|
+
return None
|
|
224
|
+
if self.dispatch_orphan_probe(state, uid, job_dict):
|
|
225
|
+
return None
|
|
226
|
+
# 关 5:stale-restore(崩溃残留消费)
|
|
227
|
+
|
|
228
|
+
# 崩溃恢复:派发子进程前先消费该 uid 的残留结果文件
|
|
229
|
+
# (上次 run 崩溃前已执行完成但未 commit)。有残留 → 直接提交,不派发。
|
|
230
|
+
# restore 在 probe **之后**——probe 通过意味着
|
|
231
|
+
# 无存活孤儿(锁生命周期 = 执行体生命周期,孤儿已死才会释放锁),
|
|
232
|
+
# 此时残留结果文件若存在必然来自已死执行的孤儿,消费它即吸收
|
|
233
|
+
# 「孤儿已完成但主进程崩溃未 commit」的执行成果,不派发(无双跑)。
|
|
234
|
+
if self._completion.restore_stale_result(uid, job, job_dict):
|
|
235
|
+
return None
|
|
236
|
+
|
|
237
|
+
# 崩溃残留声明文件污染——崩溃时序「drain 消费
|
|
238
|
+
# 结果后 _complete_job finally 前 SIGKILL」残留 outputs.jsonl,重派发
|
|
239
|
+
# 时新 worker **append** 声明 → [旧声明, 新声明] → 成功路径存在性
|
|
240
|
+
# 校验读到旧声明 → "Missing output" 假 DLQ。此处 _restore_stale_result
|
|
241
|
+
# 已返回 False(无残留结果可消费,只剩崩溃残留声明)→ submit 前
|
|
242
|
+
# 清残留;与 _complete_job finally 的防御一致(try/except OSError,
|
|
243
|
+
# FileNotFound 忽略),正常路径无残留时幂等 no-op。
|
|
244
|
+
# 清理块**必须在 probe 之后**——probe 失败(孤儿
|
|
245
|
+
# 存活)时已提前 return,绝不会走到这里删孤儿实时声明;probe 通过
|
|
246
|
+
# + restore 无果 = 无存活执行体且无残留结果,声明文件只可能是
|
|
247
|
+
# 已死执行留下的崩溃残留。
|
|
248
|
+
for _stale_decl in (
|
|
249
|
+
outputs_path(self._ctx.ipc_dir, uid), inputs_path(self._ctx.ipc_dir, uid),
|
|
250
|
+
# signals 文件同属已死执行的崩溃残留——上一执行
|
|
251
|
+
# 体写了 suspend 信号但主进程未及消费即崩,残留信号会在此刻
|
|
252
|
+
# (新 incarnation 派发前)被 apply_pending_signals 误应用到本轮
|
|
253
|
+
# 上下文(资源挂起与新执行体无关)。随声明文件一并清理。
|
|
254
|
+
signals_path(self._ctx.ipc_dir, uid),
|
|
255
|
+
):
|
|
256
|
+
try:
|
|
257
|
+
if _stale_decl.exists():
|
|
258
|
+
_stale_decl.unlink()
|
|
259
|
+
except OSError:
|
|
260
|
+
pass
|
|
261
|
+
|
|
262
|
+
# 合并 handler 默认 resources(确保 handler 注册前入队的
|
|
263
|
+
# job 也能拿到默认资源)。合并结果落在局部
|
|
264
|
+
# dict,**不回写 Job 对象**——与 runtime.inject_worker_resource
|
|
265
|
+
# 同纪律:Job 是调用方资产,派发侧不得改变其字段(否则 requeue/
|
|
266
|
+
# 复用同一 Job 实例时默认资源被永久烤入并随 to_dict 扩散)。
|
|
267
|
+
merged_resources = dict(job.resources)
|
|
268
|
+
handler_default_resources = self._ctx.handlers[job.task_type].default_resources
|
|
269
|
+
if handler_default_resources:
|
|
270
|
+
merged_resources = {**handler_default_resources, **merged_resources}
|
|
271
|
+
|
|
272
|
+
# Acquire resources (transactional: release on partial failure)
|
|
273
|
+
# acquire 循环必须在 try 块内——若第 N 个资源 acquire 抛异常
|
|
274
|
+
# (负值/NaN/自定义 Resource 校验失败),except 处理器会释放已 acquire
|
|
275
|
+
# 的前 N-1 个资源,避免永久泄漏。
|
|
276
|
+
acquired: List[Tuple[str, float]] = []
|
|
277
|
+
handle: Optional[JobHandle] = None
|
|
278
|
+
try:
|
|
279
|
+
for res_name, amount in merged_resources.items():
|
|
280
|
+
self._ctx.resources[res_name].acquire(amount)
|
|
281
|
+
acquired.append((res_name, amount))
|
|
282
|
+
|
|
283
|
+
# Payload validation
|
|
284
|
+
_handler_entry = self._ctx.handlers[job.task_type]
|
|
285
|
+
_payload_schema = _handler_entry.payload_schema
|
|
286
|
+
if _payload_schema is not None:
|
|
287
|
+
_errors = validate_payload(job.payload, _payload_schema)
|
|
288
|
+
if _errors:
|
|
289
|
+
logger.error(f"Payload validation failed for {uid}: {_errors}")
|
|
290
|
+
# 校验失败不走子进程,release 已 acquire 的资源后直接 commit
|
|
291
|
+
self._completion.release_acquired(acquired)
|
|
292
|
+
fail_meta = {"error": _ERR_PAYLOAD_VALIDATION, "details": _errors}
|
|
293
|
+
self._reject_and_commit(uid, job_dict, fail_meta)
|
|
294
|
+
return None
|
|
295
|
+
# Build context + submit (non-blocking)
|
|
296
|
+
# 增量 uid 索引(PipelineState._wall_uids/_failed_uids)避免
|
|
297
|
+
# 每次派发 ``set(state.wall.keys())`` 的 O(W) 重建;这里是单线程
|
|
298
|
+
# 派发路径,活引用在 submit 内立即 pickle 为子进程快照。
|
|
299
|
+
wall_keys = state.wall_uids
|
|
300
|
+
failed_keys = state.failed_uids
|
|
301
|
+
# 输出声明走落盘 outputs.jsonl——handler 子进程内声明的
|
|
302
|
+
# 输出经落盘文件传回主进程。
|
|
303
|
+
# fencing:分配本 job 的执行代标识(run_id.seq)。
|
|
304
|
+
# seq 每次 submit 递增——同 uid 重试再派发也获得新 incarnation,
|
|
305
|
+
# 与上次尝试的结果文件隔离(旧尝试的残留不被本次 drain 看见)。
|
|
306
|
+
self._ctx.dispatch_seq += 1
|
|
307
|
+
incarnation = f"{self._ctx.run_id}.{self._ctx.dispatch_seq}"
|
|
308
|
+
# 注册表快照契约:per-pipeline 瞬态异常注册表快照随 ctx pickle
|
|
309
|
+
# 下发——分类决策在子进程,注册表必须显式传递(不可依赖父进程
|
|
310
|
+
# 作用域,更不存在模块级可变全局)。
|
|
311
|
+
ctx = TaskContext(
|
|
312
|
+
job, wall_keys, failed_keys, dict(state.cursors),
|
|
313
|
+
output_root=self._ctx.output_root,
|
|
314
|
+
ipc_dir=self._ctx.ipc_dir, incarnation=incarnation,
|
|
315
|
+
transient_registry=self._ctx.transient_registry.snapshot(),
|
|
316
|
+
# 资源名注册集快照随 ctx 下发——
|
|
317
|
+
# suspend_resource 对未注册名 fail-loud(typo 不静默失效)。
|
|
318
|
+
resource_names=frozenset(self._ctx.resources),
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
logger.info(f"RUN: {uid}")
|
|
322
|
+
job_start = time.monotonic()
|
|
323
|
+
handle = self._ctx.executor.submit(
|
|
324
|
+
self._ctx.handlers[job.task_type].func, job, ctx, job.timeout,
|
|
325
|
+
ipc_dir=self._ctx.ipc_dir,
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
entry = InFlightJob(
|
|
329
|
+
uid=uid,
|
|
330
|
+
job_dict=job_dict,
|
|
331
|
+
job=job,
|
|
332
|
+
acquired=acquired,
|
|
333
|
+
handle=handle,
|
|
334
|
+
job_start=job_start,
|
|
335
|
+
)
|
|
336
|
+
# 在 return 前注册到 _in_flight,避免 return 后到调用方注册之间
|
|
337
|
+
# 命中 KeyboardInterrupt 导致子进程泄漏 + 资源泄漏 + job 丢失。
|
|
338
|
+
self._ctx.in_flight[uid] = entry
|
|
339
|
+
# 同步登记到 state 的 in-flight 集合(统一 is_known 事实源)
|
|
340
|
+
state.register_in_flight(uid)
|
|
341
|
+
return entry
|
|
342
|
+
|
|
343
|
+
except _CommitCrashSignal:
|
|
344
|
+
# _CommitCrashSignal 继承 BaseException——「commit 失败需崩溃」
|
|
345
|
+
# 的信号不会被 except Exception 兜底误吞。re-raise 让它穿透到
|
|
346
|
+
# run_loop 的崩溃处理分支(已 requeue 当前 job,不在此二次处理)。
|
|
347
|
+
raise
|
|
348
|
+
except KeyboardInterrupt:
|
|
349
|
+
logger.warning(f"Pipeline interrupted while dispatching {uid}.")
|
|
350
|
+
if uid in self._ctx.in_flight:
|
|
351
|
+
# entry 已注册到 _in_flight:此处不清理/不 requeue/不 release,
|
|
352
|
+
# 全部交给 _run_loop 的 _abort_in_flight 统一处理,
|
|
353
|
+
# 避免对同一 entry 二次释放资源、二次 requeue 同一作业。
|
|
354
|
+
raise
|
|
355
|
+
if handle is not None:
|
|
356
|
+
self._ctx.executor.cleanup([handle])
|
|
357
|
+
self._completion.release_acquired(acquired)
|
|
358
|
+
state.requeue_jobs([job_dict], front=True)
|
|
359
|
+
# 不在此 save_queue:内存此刻缺其他 in-flight 作业,
|
|
360
|
+
# 交给 _run_loop 的 _save_queue_crash_safe 合并磁盘真相后统一保存。
|
|
361
|
+
raise
|
|
362
|
+
except Exception as e:
|
|
363
|
+
logger.error(f"Error dispatching job {uid}: {e}\n{traceback.format_exc()}")
|
|
364
|
+
if handle is not None:
|
|
365
|
+
self._ctx.executor.cleanup([handle])
|
|
366
|
+
self._completion.release_acquired(acquired)
|
|
367
|
+
# dispatch 阶段失败(submit 的 pickle/启动报错、
|
|
368
|
+
# 资源 acquire 校验失败):确定性失败(如不可 pickle 的
|
|
369
|
+
# lambda handler)若只 requeue + 崩溃会触发**无限重启循环**。
|
|
370
|
+
# 独立 `_dispatch_failures` 计数——不复用
|
|
371
|
+
# `_commit_failures`(混用计数的话,dispatch 失败几次后任意一次
|
|
372
|
+
# commit 失败即达阈值进 DLQ,错误码 COMMIT_FAILURE_DLQ 误导
|
|
373
|
+
raw_rt = job_dict.get("runtime")
|
|
374
|
+
if not isinstance(raw_rt, dict):
|
|
375
|
+
raw_rt = {}
|
|
376
|
+
job_dict["runtime"] = raw_rt
|
|
377
|
+
rt = raw_rt
|
|
378
|
+
failures = rt.get("_dispatch_failures", 0) + 1
|
|
379
|
+
rt["_dispatch_failures"] = failures
|
|
380
|
+
if failures >= self._ctx.commit_failure_dlq_threshold:
|
|
381
|
+
logger.critical(
|
|
382
|
+
f"Dispatch failed {failures} times for {uid} ({e}); "
|
|
383
|
+
f"treating as deterministic bad input (e.g. unpickleable "
|
|
384
|
+
f"handler), sending to DLQ."
|
|
385
|
+
)
|
|
386
|
+
committed = self._ctx.backend.commit_job_failure(
|
|
387
|
+
uid, {"error": _ERR_DISPATCH_FAILURE,
|
|
388
|
+
"failures": failures, "detail": str(e)[:200]},
|
|
389
|
+
)
|
|
390
|
+
if committed:
|
|
391
|
+
# 与下方 requeue 分支对称——DLQ 分支
|
|
392
|
+
# return 前也移除 entry:若 entry 已注册(register_in_flight
|
|
393
|
+
# 的 DEBUG 断言失败时可达),残留的 entry 会在后续 drain
|
|
394
|
+
# 中被当 in-flight 处理(死 handle → 二次 _complete_job →
|
|
395
|
+
# 重复 DLQ / 断言崩)。未注册时 pop/unregister 均安全。
|
|
396
|
+
self._ctx.in_flight.pop(uid, None)
|
|
397
|
+
# dispatch 失败达阈值视为业务侧确定性坏输入,下游自动级联跳过。
|
|
398
|
+
self._failure.apply_failed(
|
|
399
|
+
uid, {"error": _ERR_DISPATCH_FAILURE,
|
|
400
|
+
"failures": failures, "detail": str(e)[:200]}
|
|
401
|
+
)
|
|
402
|
+
self._failure.cascade_fail(uid)
|
|
403
|
+
# dispatch 3-strike 终态触发钩子——
|
|
404
|
+
# 承诺「每个 job 终结时钩子恰好调用一次」(否则监控
|
|
405
|
+
# 漏报该类失败)。与 _commit_failed_crash /
|
|
406
|
+
# payload 校验等直接 commit 路径对称。
|
|
407
|
+
self._ctx.fire_job_completed(
|
|
408
|
+
uid, {"error": _ERR_DISPATCH_FAILURE,
|
|
409
|
+
"failures": failures, "detail": str(e)[:200]},
|
|
410
|
+
False, False,
|
|
411
|
+
)
|
|
412
|
+
return
|
|
413
|
+
# DLQ 也失败(环境故障)→ 走 crash 路径
|
|
414
|
+
# 与 KeyboardInterrupt 分支对称——若 entry 已
|
|
415
|
+
# 注册到 _in_flight(仅 register_in_flight 的 DEBUG 断言失败时
|
|
416
|
+
# 可达,此时状态已损坏),requeue 后 raise 会被 _run_loop 的
|
|
417
|
+
# _abort_in_flight 对该 entry **二次 requeue**(内存队列重复 uid)。
|
|
418
|
+
# 先移除 entry(未注册时 pop/unregister 均安全),requeue 交给
|
|
419
|
+
# 下方统一执行。
|
|
420
|
+
self._ctx.in_flight.pop(uid, None)
|
|
421
|
+
state.unregister_in_flight(uid)
|
|
422
|
+
state.requeue_jobs([job_dict], front=True)
|
|
423
|
+
# 不在此 save_queue:内存此刻缺其他 in-flight 作业,
|
|
424
|
+
# 交给 _run_loop 的 _save_queue_crash_safe 合并磁盘真相后统一保存。
|
|
425
|
+
raise
|
|
426
|
+
|