feat: 任务断点续跑/暂停中断/LLM 过滤优化与调度容错
调度与状态机: - 修复 PAUSED 任务被拾起后复活执行(点击暂停反而开始任务):next_queued_run 只取 QUEUED,execute_run 以 PAUSED 进入直接返回,暂停必须显式 resume - 重启恢复:启动时 recover_interrupted_runs 把遗留 RUNNING 置 QUEUED(保留产物) - 暂停信号 paused.flag:暂停接口写、继续/重试清除,OCR 逐帧检查秒级中断, 节点内被暂停保持 PAUSED 不误报 FAILED - 调度轮询容错:_loop 异常不杀死线程(曾致任务永久停留 QUEUED) subtitle-ocr 节点级断点: - ocr_partial.jsonl 逐帧存档,重启/暂停后只处理未处理帧,产物与一次跑完一致 - 进度日志携带窗口平均耗时与线程数;取消后抑制进度日志井喷 llm-filter 过滤质量与限流自适应: - 上下文净化:喂给 LLM 的是过滤后的字幕(规则层垃圾从上下文剔除) - 正则确定性过滤:裸网址域名、HTML/水印模式直接删除 - 429/5xx 指数退避重试;worker 限流错误 report_failure 内存临时降最大线程数 并缩容(无错误窗口回升),失败条目降并发后重试一轮 - 保留长文本保护(noise 不删 ≥min_keep_len 文本,LLM 判定不稳的必要兜底) 前端: - 工作流编排页支持选择工作流编辑(加载最新/历史版本)、版本历史面板、 新建/编辑双模式;管理后台编辑跳转 workflow.html?edit=<id> 工作流:ocr-subtitle v7(filter pool_max_workers=20、pool_fast_threshold=1)
This commit is contained in:
+72
-4
@@ -55,7 +55,7 @@ class AdaptiveThreadPool:
|
||||
fast_threshold: float = 0.3,
|
||||
slow_threshold: float = 1.0,
|
||||
clock=time.monotonic,
|
||||
on_progress: Callable[[int, int, float], None] | None = None,
|
||||
on_progress: Callable[[int, int, float, float, int], None] | None = None,
|
||||
) -> None:
|
||||
"""初始化;clock 可注入便于测试;on_progress(done,total,rate) 每次完成回调。"""
|
||||
self._worker = worker
|
||||
@@ -73,6 +73,15 @@ class AdaptiveThreadPool:
|
||||
self._results: list = []
|
||||
self._lock = threading.Lock()
|
||||
self._stop = threading.Event()
|
||||
# 取消标记:worker 检测到取消(如暂停信号)后设置,后续完成的任务
|
||||
# 不再触发进度回调——暂停时队列中剩余大量任务会快速退出,若仍逐项
|
||||
# 打印进度会在数秒内打出上万行日志。
|
||||
self._cancel_event = threading.Event()
|
||||
# 有效最大线程数:初始等于 max_workers;消费错误(如 API 限流)时
|
||||
# report_failure 临时收紧,连续无错误窗口后逐步回升——并发自适应配额。
|
||||
self._effective_max_workers = max_workers
|
||||
# 当前窗口内消费错误计数:窗口评估时无错误才允许恢复有效上限。
|
||||
self._window_failures = 0
|
||||
# 滚动窗口起点与已记录的单次耗时。
|
||||
self._window_start = clock()
|
||||
# 观测到的最大并发线程数(供测试与监控)。
|
||||
@@ -83,6 +92,9 @@ class AdaptiveThreadPool:
|
||||
self._total = 0
|
||||
self._started_at = 0.0
|
||||
self._window_times: list[float] = []
|
||||
# 最近一次窗口评估的平均单任务耗时(秒):供进度回调诊断使用,
|
||||
# 与扩缩容决策共用同一依据;窗口尚未评估时为 None(回退累计平均)。
|
||||
self._window_avg_time: float | None = None
|
||||
|
||||
def _run(self) -> None:
|
||||
"""工作线程主循环:取任务 → 执行 → 记录耗时并自适应评估。"""
|
||||
@@ -107,10 +119,16 @@ class AdaptiveThreadPool:
|
||||
self._results.append((seq, result))
|
||||
# 进度回调:已完成数、总数与平均处理速度(条/秒)。
|
||||
self._completed += 1
|
||||
if self._on_progress is not None:
|
||||
if self._on_progress is not None and not self._cancel_event.is_set():
|
||||
elapsed_total = max(self._clock() - self._started_at, 1e-9)
|
||||
with self._lock:
|
||||
workers = self._target_workers
|
||||
self._on_progress(
|
||||
self._completed, self._total, self._completed / elapsed_total
|
||||
self._completed,
|
||||
self._total,
|
||||
self._completed / elapsed_total,
|
||||
self._current_avg_time(elapsed_total),
|
||||
workers,
|
||||
)
|
||||
self._tick(elapsed)
|
||||
self._queue.task_done()
|
||||
@@ -128,15 +146,36 @@ class AdaptiveThreadPool:
|
||||
avg = sum(self._window_times) / len(self._window_times)
|
||||
self._window_start = self._clock()
|
||||
self._window_times.clear()
|
||||
# 记录本次窗口平均耗时:进度回调据此展示"当前扩缩容依据"。
|
||||
self._window_avg_time = avg
|
||||
with self._lock:
|
||||
current = self._target_workers
|
||||
# 窗口内无消费错误 → 有效上限逐步回升(错误降下来的并发慢慢恢复)。
|
||||
if (
|
||||
self._window_failures == 0
|
||||
and self._effective_max_workers < self.max_workers
|
||||
):
|
||||
self._effective_max_workers += 1
|
||||
# 重置窗口错误计数,进入下一窗口。
|
||||
self._window_failures = 0
|
||||
# 扩容上限用有效最大线程数:错误窗口内即使响应快也不超过收紧后的上限。
|
||||
self._resize(
|
||||
decide(
|
||||
current, avg, self.min_workers, self.max_workers,
|
||||
current, avg, self.min_workers, self._effective_max_workers,
|
||||
self.fast_threshold, self.slow_threshold,
|
||||
)
|
||||
)
|
||||
|
||||
def _current_avg_time(self, elapsed_total: float) -> float:
|
||||
"""返回供进度回调展示的平均单任务耗时(秒)。
|
||||
|
||||
优先使用最近一次窗口评估的平均耗时(与扩缩容决策同一依据);
|
||||
窗口尚未评估过时回退为启动至今的累计平均,避免无数据可看。
|
||||
"""
|
||||
if self._window_avg_time is not None:
|
||||
return self._window_avg_time
|
||||
return elapsed_total / max(self._completed, 1)
|
||||
|
||||
def _resize(self, target: int) -> None:
|
||||
"""调整并发目标:扩容启动新线程;缩容压入等量停止哨兵(幂等)。
|
||||
|
||||
@@ -157,6 +196,28 @@ class AdaptiveThreadPool:
|
||||
self._queue.put((None, _POISON))
|
||||
self._target_workers = target
|
||||
|
||||
def cancel(self) -> None:
|
||||
"""请求取消本批任务:后续完成的任务不再触发进度回调。
|
||||
|
||||
供调用方在工作线程内检测到外部信号(如暂停)时调用,抑制暂停后
|
||||
队列中剩余任务快速退出导致的进度日志井喷;下一批 map 自动重置。
|
||||
"""
|
||||
self._cancel_event.set()
|
||||
|
||||
def report_failure(self) -> None:
|
||||
"""通知一次消费错误(如 API 限流 429):临时降低有效最大线程数并缩容。
|
||||
|
||||
供工作线程捕获可退避错误(限流/服务端 5xx)后调用:并发立即收紧到
|
||||
新上限,后续请求减少从而避开持续限流;连续无错误窗口后有效上限
|
||||
逐步回升到 max_workers(见 _tick 的恢复逻辑)。
|
||||
"""
|
||||
with self._lock:
|
||||
self._window_failures += 1
|
||||
if self._effective_max_workers > self.min_workers:
|
||||
self._effective_max_workers -= 1
|
||||
# 缩容到新上限(幂等:目标低于当前才放停止哨兵)。
|
||||
self._resize(self._effective_max_workers)
|
||||
|
||||
def map(self, items) -> list:
|
||||
"""按输入顺序返回每个 item 经 worker 处理后的结果列表。"""
|
||||
self._results = []
|
||||
@@ -164,6 +225,13 @@ class AdaptiveThreadPool:
|
||||
self._total = len(items)
|
||||
self._started_at = self._clock()
|
||||
self._stop.clear()
|
||||
# 每批任务开始时重置取消状态:上一批的取消不延续到下一批。
|
||||
self._cancel_event.clear()
|
||||
# 上一批任务结束后工作线程已全部退出(_stop 停止)但 _target_workers
|
||||
# 仍记旧值,_resize 不会重新启动线程——实际无线程时归零后重建。
|
||||
with self._lock:
|
||||
if not self._threads:
|
||||
self._target_workers = 0
|
||||
self._resize(self.min_workers)
|
||||
for seq, item in enumerate(items):
|
||||
self._queue.put((seq, item))
|
||||
|
||||
Reference in New Issue
Block a user