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
tasklite/engine/loop.py
ADDED
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
"""主循环机器:run loop 与异常承重网。
|
|
2
|
+
|
|
3
|
+
事件驱动主循环:填池 → drain → 等待。``run_loop`` 是全部退出路径的
|
|
4
|
+
承重网(normal/interrupt/commit-crash/unknown 四分支清理对称 + suspend
|
|
5
|
+
持久化 + on_run_end 单一出口);``run_loop_impl`` 是纯调度编排。
|
|
6
|
+
依赖经 RunContext 注入,经 recovery/dispatch/completion/failure 机器
|
|
7
|
+
协同,不反向引用 TaskLite。
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
import time
|
|
12
|
+
import traceback
|
|
13
|
+
from typing import TYPE_CHECKING
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from .runtime import RunContext
|
|
17
|
+
from .recovery import RecoveryMachine
|
|
18
|
+
from .dispatch import DispatchMachine
|
|
19
|
+
from .failure import FailureMachine
|
|
20
|
+
from .completion import CompletionMachine
|
|
21
|
+
|
|
22
|
+
from ..exceptions import _CommitCrashSignal, _JobTerminated
|
|
23
|
+
from .runtime import StopMode, WORKER_RESOURCE
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger("tasklite")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class LoopRunner:
|
|
29
|
+
"""填池/回收/等待的主循环 + 四退出路径承重网。"""
|
|
30
|
+
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
ctx: "RunContext",
|
|
34
|
+
recovery: "RecoveryMachine",
|
|
35
|
+
dispatch: "DispatchMachine",
|
|
36
|
+
failure: "FailureMachine",
|
|
37
|
+
completion: "CompletionMachine",
|
|
38
|
+
) -> None:
|
|
39
|
+
self._ctx = ctx
|
|
40
|
+
self._recovery = recovery
|
|
41
|
+
self._dispatch = dispatch
|
|
42
|
+
self._failure = failure
|
|
43
|
+
self._completion = completion
|
|
44
|
+
|
|
45
|
+
def run_loop(self) -> None:
|
|
46
|
+
"""Run loop with exception handling.
|
|
47
|
+
|
|
48
|
+
异常退出时先 ``_abort_in_flight`` kill 所有 in-flight 子进程并 requeue
|
|
49
|
+
未 commit 的 job,再 ``_save_queue_crash_safe``(以磁盘为基准合并内存,
|
|
50
|
+
避免 commit/pop 窗口内丢失作业),然后 re-raise。
|
|
51
|
+
|
|
52
|
+
兜底契约:``_JobTerminated`` 逃逸至此属承重网
|
|
53
|
+
路径(正常路径已被 _dispatch_job/_complete_job 内层承接,本分支实际
|
|
54
|
+
不可达)——但**同样**先 abort + save 再 re-raise,与其他三分支对称:
|
|
55
|
+
逃逸意味着契约被破坏,in-flight 子进程/内存队列不可信,必须先清理。
|
|
56
|
+
只有 ``_CommitCrashSignal`` 与未知异常走相同的崩溃契约。
|
|
57
|
+
"""
|
|
58
|
+
# on_run_end 覆盖全部退出路径——正常/中断/崩溃统一
|
|
59
|
+
# 在 finally 触发,签名带 exit_reason(监控最不能丢的是崩溃事件)。
|
|
60
|
+
exit_reason = "completed"
|
|
61
|
+
try:
|
|
62
|
+
self.run_loop_impl()
|
|
63
|
+
# stop 请求的退出不是「completed」——监控/
|
|
64
|
+
# on_run_end 消费方据此区分自然排空与人为停机(draining=等完
|
|
65
|
+
# 在途后退出;aborting=强杀在途立即退出)。
|
|
66
|
+
if self._ctx.stop_mode is StopMode.ABORTING:
|
|
67
|
+
exit_reason = "stopped_aborting"
|
|
68
|
+
elif self._ctx.stop_mode is StopMode.DRAINING:
|
|
69
|
+
exit_reason = "stopped_draining"
|
|
70
|
+
except _JobTerminated as e:
|
|
71
|
+
# 正常路径内层已捕获(_dispatch_job/
|
|
72
|
+
# _complete_job 各自 `except _JobTerminated` 承接,job 已终结 →
|
|
73
|
+
# 返回 None,主循环继续),本分支**实际不可达**,属防御性
|
|
74
|
+
# fail-loud 承重网:若未来改动新增 `_commit_failed_crash` 直调点
|
|
75
|
+
# 而漏包 except,`_JobTerminated` 逃逸至此**上抛暴露**(run
|
|
76
|
+
# 崩溃可见)而非静默吞掉(静默会掩盖类漏洞——已 DLQ 的
|
|
77
|
+
# job 被 except Exception requeue 复发)。
|
|
78
|
+
# 与其他三分支对称执行清理——逃逸
|
|
79
|
+
# 时 in-flight 子进程仍在跑(资源占用/副作用未释放),必须先
|
|
80
|
+
# abort + save 再上抛,避免「崩溃可见但资源泄漏 + 内存队列丢失」。
|
|
81
|
+
logger.critical(f"Job terminated outside expected handlers: {e}")
|
|
82
|
+
exit_reason = "error"
|
|
83
|
+
self._recovery.abort_in_flight()
|
|
84
|
+
self._recovery.save_queue_crash_safe()
|
|
85
|
+
raise
|
|
86
|
+
except KeyboardInterrupt:
|
|
87
|
+
logger.warning("Pipeline interrupted by user.")
|
|
88
|
+
exit_reason = "interrupted"
|
|
89
|
+
self._recovery.abort_in_flight()
|
|
90
|
+
self._recovery.save_queue_crash_safe()
|
|
91
|
+
raise
|
|
92
|
+
except _CommitCrashSignal as e:
|
|
93
|
+
# commit 失败崩溃契约(设计内路径):清理 in-flight 后按序退出。
|
|
94
|
+
logger.critical(f"Backend commit failure; aborting in-flight jobs: {e}")
|
|
95
|
+
exit_reason = "error"
|
|
96
|
+
self._recovery.abort_in_flight()
|
|
97
|
+
self._recovery.save_queue_crash_safe()
|
|
98
|
+
raise
|
|
99
|
+
except Exception as e:
|
|
100
|
+
logger.critical(f"Pipeline scheduler crashed with unhandled exception: {e}\n{traceback.format_exc()}")
|
|
101
|
+
exit_reason = "error"
|
|
102
|
+
self._recovery.abort_in_flight()
|
|
103
|
+
self._recovery.save_queue_crash_safe()
|
|
104
|
+
raise
|
|
105
|
+
except BaseException as e:
|
|
106
|
+
# 兜底分支——非 KI 的 BaseException(handler/
|
|
107
|
+
# 钩子主动 raise SystemExit、自定义取消异常等)逃逸时同样执行
|
|
108
|
+
# 清理契约(abort + crash-safe save),不留泄漏子进程/内存队列
|
|
109
|
+
# 丢失;不吞异常,清理后 re-raise 保持崩溃可见。
|
|
110
|
+
logger.critical(
|
|
111
|
+
f"Pipeline terminated by {type(e).__name__}: {e}"
|
|
112
|
+
)
|
|
113
|
+
exit_reason = "error"
|
|
114
|
+
self._recovery.abort_in_flight()
|
|
115
|
+
self._recovery.save_queue_crash_safe()
|
|
116
|
+
raise
|
|
117
|
+
finally:
|
|
118
|
+
# 单一出口:资源挂起持久化挂统一 finally——正常退出、
|
|
119
|
+
# KeyboardInterrupt、_CommitCrashSignal、未知异常、BaseException
|
|
120
|
+
# 兜底五路径一致持久化。
|
|
121
|
+
# 崩溃路径若不在此持久化,429
|
|
122
|
+
# 限流状态丢失 → 重启后恢复对 API 的猛打。
|
|
123
|
+
# 自身 try/except + warning,不遮蔽原异常(原异常由上面 re-raise)。
|
|
124
|
+
try:
|
|
125
|
+
self._recovery.persist_resource_suspends()
|
|
126
|
+
except Exception as e:
|
|
127
|
+
logger.warning(f"Failed to persist resource suspends: {e}")
|
|
128
|
+
# on_run_end 同 finally 触发(含崩溃路径),
|
|
129
|
+
# 异常隔离——钩子按不可信代码对待,不遮蔽原异常。
|
|
130
|
+
self._ctx.fire_run_end(exit_reason)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def run_loop_impl(self) -> None:
|
|
134
|
+
"""事件驱动主循环:填池 → drain → 等待。直接操作 self._ctx.state。
|
|
135
|
+
|
|
136
|
+
``self._ctx.in_flight`` 记录已派发到子进程、尚未 commit 的 job。
|
|
137
|
+
循环直到队列空且 in-flight 空。
|
|
138
|
+
"""
|
|
139
|
+
state = self._ctx.state
|
|
140
|
+
self._ctx.in_flight = {}
|
|
141
|
+
while not state.is_empty or self._ctx.in_flight:
|
|
142
|
+
# 调度缓存为 run 生命周期(_run_body
|
|
143
|
+
# 加载期清空),主循环不每轮清空——阻塞/慢 job 阶段不重复反
|
|
144
|
+
# 序列化全队列。
|
|
145
|
+
draining = False
|
|
146
|
+
if self._ctx.stop_mode is not StopMode.NONE:
|
|
147
|
+
if self._ctx.stop_mode is StopMode.ABORTING:
|
|
148
|
+
# ABORTING:kill 全部 in-flight + 清理半成品输出 + requeue
|
|
149
|
+
logger.warning("Force abort requested. Killing in-flight jobs.")
|
|
150
|
+
self._recovery.abort_in_flight()
|
|
151
|
+
self._recovery.save_queue_crash_safe()
|
|
152
|
+
break
|
|
153
|
+
if self._ctx.in_flight:
|
|
154
|
+
# DRAINING:等 in-flight 自然完成——本轮不派发新 job,
|
|
155
|
+
# 也不 kill,落到下方 drain 回收结果;队列中未派发的
|
|
156
|
+
# job 保留到下次 run。
|
|
157
|
+
draining = True
|
|
158
|
+
else:
|
|
159
|
+
# DRAINING 且 in-flight 已空:保存队列退出
|
|
160
|
+
logger.info("Pipeline drained. Saving queue and exiting.")
|
|
161
|
+
self._recovery.save_queue_crash_safe()
|
|
162
|
+
break
|
|
163
|
+
|
|
164
|
+
# 1. 填池:循环派发直到 __workers__ 资源耗尽或无 runnable
|
|
165
|
+
# DRAINING 时跳过填池——不再派发新 job,只等 in-flight 回收。
|
|
166
|
+
sched = None
|
|
167
|
+
worker_wait = 0.0
|
|
168
|
+
if not draining:
|
|
169
|
+
while True:
|
|
170
|
+
# workers 耗尽预检——本轮不可能
|
|
171
|
+
# 有可派发 job(需 worker 槽的),跳过全队列扫描
|
|
172
|
+
# (N=10 万时每轮 ~300ms CPU 空烧,慢 job 阶段 20Hz
|
|
173
|
+
# 轮询 ~85% 单核)。no-subprocess 路径(dedup/dep-failed/
|
|
174
|
+
# no-handler)的清理延迟至 worker 释放,最终仍会处理。
|
|
175
|
+
ok, worker_wait = self._ctx.resources[WORKER_RESOURCE].can_acquire(1.0)
|
|
176
|
+
if not ok:
|
|
177
|
+
# 忙循环护栏:第三方自定义资源在
|
|
178
|
+
# 不可用态可能返回 wait=0(内置 CapacityResource /
|
|
179
|
+
# RateLimitResource 恒返回 >0,不受影响)。若此处不
|
|
180
|
+
# 钳制下界,下方 `elif worker_wait > 0` 睡眠分支全部
|
|
181
|
+
# 不命中 → 无限忙循环空烧 CPU(stop 响应也下降)。
|
|
182
|
+
# 钳制为最小轮询间隔保证至少有一次睡眠。
|
|
183
|
+
if worker_wait <= 0:
|
|
184
|
+
worker_wait = 0.05
|
|
185
|
+
break
|
|
186
|
+
# in_flight 以 state 集合为事实源(与 is_known 一致)
|
|
187
|
+
in_flight_uids = state.in_flight_uids
|
|
188
|
+
sched = self._ctx.scheduler.pop_next_runnable(
|
|
189
|
+
state, in_flight_uids
|
|
190
|
+
)
|
|
191
|
+
if sched.runnable_idx is None:
|
|
192
|
+
break
|
|
193
|
+
entry = self._dispatch.dispatch_job(sched)
|
|
194
|
+
if entry is None:
|
|
195
|
+
continue # 依赖失败/no-handler/payload 校验失败:已直接处理
|
|
196
|
+
# entry 已在 _dispatch_job 内注册到 _in_flight(避免窗口泄漏)
|
|
197
|
+
# __workers__ 资源耗尽时下一轮扫描无 runnable,自然退出
|
|
198
|
+
|
|
199
|
+
# 处理无可运行 job 的情况
|
|
200
|
+
if sched is not None and sched.runnable_idx is None:
|
|
201
|
+
if state.is_empty and not self._ctx.in_flight:
|
|
202
|
+
# 队列已排空且无 in-flight:正常完成,非死锁。
|
|
203
|
+
# 走「不走子进程」路径(stale 恢复/校验失败)清空队列时,
|
|
204
|
+
# 外层 while 条件只在循环头检查,此处需显式退出,
|
|
205
|
+
# 避免空队列被误判为死锁(假日志 + 空操作 break)。
|
|
206
|
+
break
|
|
207
|
+
if sched.min_wait == float('inf'):
|
|
208
|
+
# 死锁判定:仅当 in_flight 为空时才是真死锁。
|
|
209
|
+
# in_flight 非空时资源可能被释放解锁,不判死锁,落到 drain 等待。
|
|
210
|
+
if not self._ctx.in_flight:
|
|
211
|
+
should_break = self._failure.handle_deadlock(sched)
|
|
212
|
+
if should_break:
|
|
213
|
+
break
|
|
214
|
+
continue
|
|
215
|
+
# min_wait 有限(资源暂不可用/backoff)或 in_flight 非空:drain 等待
|
|
216
|
+
|
|
217
|
+
# 2. drain:非阻塞收集已完成结果
|
|
218
|
+
if self._ctx.in_flight:
|
|
219
|
+
# 读取 in-flight 的 suspend 信号文件,即时应用
|
|
220
|
+
# (handler 崩溃/超时也不丢失限流信息——文件落盘)
|
|
221
|
+
self._recovery.apply_pending_signals()
|
|
222
|
+
handles = [entry.handle for entry in self._ctx.in_flight.values()]
|
|
223
|
+
completed = self._ctx.executor.reap_completed(handles)
|
|
224
|
+
for handle, result in completed:
|
|
225
|
+
# 先 complete 再 pop:complete 与 pop 之间被
|
|
226
|
+
# Ctrl+C/KI 打断时 entry 仍在 in_flight——abort_in_flight
|
|
227
|
+
# 会正确释放资源并 requeue(job 未 commit,at-least-once)。
|
|
228
|
+
# 若先 pop,entry 已离开 in_flight 而
|
|
229
|
+
# CapacityResource.used 要到 complete_job 内部才释放
|
|
230
|
+
# → 跨 run 泄漏 worker 槽位,管线静默活锁。
|
|
231
|
+
# pop 放 try/finally:commit 失败路径 complete_job 内
|
|
232
|
+
# commit_failed_crash 已自行 requeue 并抛
|
|
233
|
+
# _CommitCrashSignal——若不 pop,stale entry 会被 abort
|
|
234
|
+
# 二次 requeue(无 wall 记录可吸收,同一 job 双重入队)。
|
|
235
|
+
entry = self._ctx.in_flight.get(handle.uid)
|
|
236
|
+
try:
|
|
237
|
+
self._completion.complete_job(entry, result)
|
|
238
|
+
finally:
|
|
239
|
+
self._ctx.in_flight.pop(handle.uid, None)
|
|
240
|
+
|
|
241
|
+
# 3. 无新完成且仍有 in-flight → 短轮询等待。
|
|
242
|
+
# 用短间隔(50ms)而非 sched.min_wait,因为 job 可能在任意时刻
|
|
243
|
+
# 完成需要及时回收;长 sleep 会抵消并发收益。
|
|
244
|
+
if not completed and self._ctx.in_flight:
|
|
245
|
+
time.sleep(0.05)
|
|
246
|
+
elif sched is not None and sched.runnable_idx is None and sched.min_wait != float('inf'):
|
|
247
|
+
# 退避/资源等待可能遮蔽依赖环死锁——环成员处于退避时
|
|
248
|
+
# min_wait 有限,主循环无限 sleep,死锁判定被推迟到退避结束
|
|
249
|
+
# (指数退避可拖数十分钟)。
|
|
250
|
+
# 无 in-flight 且存在等待依赖时,即使 min_wait 有限也先做环检测;
|
|
251
|
+
# 确认有环才走死锁处理(只失败环成员),否则是合法依赖链尾退避,照常等待。
|
|
252
|
+
if not self._ctx.in_flight and sched.waiting_for_dependency:
|
|
253
|
+
cycle_uids = state.find_dependency_cycles()
|
|
254
|
+
if cycle_uids:
|
|
255
|
+
logger.error(
|
|
256
|
+
f"Deadlock detected during backoff/wait: dependency cycle "
|
|
257
|
+
f"{sorted(set(cycle_uids))} masked by finite min_wait."
|
|
258
|
+
)
|
|
259
|
+
sched.min_wait = float('inf')
|
|
260
|
+
should_break = self._failure.handle_deadlock(sched)
|
|
261
|
+
if should_break:
|
|
262
|
+
break
|
|
263
|
+
continue
|
|
264
|
+
# 无 in-flight 但需等待(backoff / 资源限流释放):sleep(min_wait)。
|
|
265
|
+
# 支持 backoff 作业的等待路径。
|
|
266
|
+
# cap 1.0s 避免 backoff 时间过长时无法响应停机请求。
|
|
267
|
+
time.sleep(min(sched.min_wait, 1.0))
|
|
268
|
+
elif worker_wait > 0 and not self._ctx.in_flight:
|
|
269
|
+
# workers 预检 break 且无 in-flight 可
|
|
270
|
+
# drain——__workers__ 被 suspend(ctx.suspend_resource 或
|
|
271
|
+
# 持久化恢复)或用户覆盖为 capacity<1 / RateLimitResource 时,
|
|
272
|
+
# can_acquire 返回 False 且队列无在途 job:无此分支则
|
|
273
|
+
# sched=None 使两个等待分支都跳过 → 无限忙循环。
|
|
274
|
+
# cap 1.0s 保持停机请求响应性。
|
|
275
|
+
time.sleep(min(worker_wait, 1.0))
|
|
276
|
+
|
|
277
|
+
logger.info(f"Pipeline {self._ctx.name} finished.")
|
|
278
|
+
# run 结束时统一持久化一次资源挂起状态——由 _run_loop 的
|
|
279
|
+
# finally 统一执行(正常/崩溃路径一致,见 _run_loop)。
|
|
280
|
+
self._ctx.in_flight = {}
|
|
281
|
+
|