base_agent/uas/task/task_executor.py

860 lines
35 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# -*- coding: utf-8 -*-
"""
Task 执行器
─────────────────────────────────────────────────────────────────
基于 PyQt5 QObject/pyqtSignal + QThreadPool 实现:
核心调度逻辑:
1. 任务启动时,按时间轴扫描航线各段
2. 进入航段时:
a. 并行组 → 同时提交所有 Action 到 ActionExecutor
b. 串行组 → 按 order 顺序依次提交,等待上一个完成再提交下一个
c. 串/并行组的执行顺序由 serial_first 控制
3. 航线级 Action 在整条航线 ON_ENTER/ON_EXIT 时触发
4. CONTINUOUS 类型 Action 由 QTimer 周期触发
5. SCHEDULED 类型 Action 由 QTimer.singleShot 在指定偏移时刻触发
─────────────────────────────────────────────────────────────────
"""
from __future__ import annotations
import functools
import logging
import threading
import time
from typing import Dict, List, Optional
from PyQt5.QtCore import (
QObject, QRunnable, QThreadPool,
pyqtSignal, pyqtSlot, QTimer
)
from uas.event.event_handler import on_event
from uas.event.event_model import Event
from uas.event.event_type import EventType
from uas.task.task_action_plan import (
ActionEntry, SegmentActionPlan, RouteActionPlan,
TriggerMoment
)
from uas.task.task_model import Task, TaskState
logger = logging.getLogger(__name__)
# ─────────────────────────────────────────────────────────────────
# 串行 Action 链执行器(在工作线程中顺序执行)
# ─────────────────────────────────────────────────────────────────
class _SerialChainSignals(QObject):
finished = pyqtSignal(str, list) # task_id, [ActionResult]
error = pyqtSignal(str, str) # task_id, error_msg
class _SerialChainWorker(QRunnable):
"""
在 QThreadPool 工作线程中串行执行一组 ActionEntry
每个 Action 完成后才执行下一个(严格顺序保证)
"""
def __init__(
self,
task_id: str,
entries: List[ActionEntry],
executor, # ActionExecutor 引用
label: str = "", # 调试标签(如 "route" / "seg[0]"
):
super().__init__()
self.task_id = task_id
self.entries = entries # 已按 order 排序
self.executor = executor
self.label = label
self.signals = _SerialChainSignals()
self.setAutoDelete(True)
@pyqtSlot()
def run(self):
results = []
for entry in self.entries:
if not entry.enabled:
continue
try:
logger.debug(
f"[串行/{self.label}] 执行 Action: "
f"{entry.action.type_key} order={entry.order}"
)
# submit_sync 阻塞当前工作线程,保证严格串行
result = self.executor.submit_sync(entry.action)
results.append(result)
logger.debug(
f"[串行/{self.label}] 完成 Action: "
f"{entry.action.type_key} success={result.success}"
)
except Exception as e:
logger.error(
f"[串行/{self.label}] Action 执行异常: "
f"{entry.action.type_key}{e}",
exc_info=True,
)
self.signals.error.emit(self.task_id, str(e))
return # 串行链中断
self.signals.finished.emit(self.task_id, results)
# ─────────────────────────────────────────────────────────────────
# 航段调度器(管理单个航段的 Action 触发)
# ─────────────────────────────────────────────────────────────────
class _SegmentScheduler(QObject):
"""
管理单个航段的 Action 调度
负责ON_ENTER 触发、CONTINUOUS 定时器、SCHEDULED 单次定时器、ON_EXIT 触发
继承 QObject 以便在内部安全使用 QTimer
"""
serial_done = pyqtSignal(str, int, list) # task_id, seg_index, results
serial_error = pyqtSignal(str, int, str) # task_id, seg_index, error
def __init__(
self,
task: Task,
seg_index: int,
plan: SegmentActionPlan,
action_executor,
thread_pool: QThreadPool,
parent: QObject = None,
):
super().__init__(parent)
self.task = task
self.seg_index = seg_index
self.plan = plan
self.action_executor = action_executor
self.pool = thread_pool
self._timers: List[QTimer] = []
self._active = False
# ── 进入航段 ──────────────────────────────────────────────────
def start(self):
"""进入航段时调用:触发 ON_ENTER + 启动定时器"""
self._active = True
logger.info(
f"[Seg{self.seg_index}] 进入,启动 Action 调度 "
f"task={self.task.task_id[:8]}"
)
self.task.add_record(
"action_start",
f"进入航段[{self.seg_index}],开始 Action 调度",
)
# ON_ENTER 触发
on_enter = self.plan.entries_by_trigger(TriggerMoment.ON_ENTER)
if on_enter:
self._dispatch_entries(on_enter, label=f"seg{self.seg_index}/enter")
# SCHEDULED 触发(单次定时器)
for entry in self.plan.entries_by_trigger(TriggerMoment.SCHEDULED):
delay_ms = max(0, int(entry.time_offset_s * 1000))
t = QTimer(self)
t.setSingleShot(True)
t.timeout.connect(lambda e=entry: self._dispatch_single(e))
t.start(delay_ms)
self._timers.append(t)
logger.debug(
f"[Seg{self.seg_index}] SCHEDULED Action "
f"{entry.action.type_key} 将在 {delay_ms}ms 后触发"
)
# CONTINUOUS 触发(周期定时器)
for entry in self.plan.entries_by_trigger(TriggerMoment.CONTINUOUS):
interval_ms = max(100, int(entry.interval_s * 1000))
t = QTimer(self)
t.timeout.connect(lambda e=entry: self._dispatch_single(e))
t.start(interval_ms)
self._timers.append(t)
logger.debug(
f"[Seg{self.seg_index}] CONTINUOUS Action "
f"{entry.action.type_key}{interval_ms}ms 触发"
)
# ── 离开航段 ──────────────────────────────────────────────────
def stop(self):
"""离开航段时调用:停止所有定时器 + 触发 ON_EXIT"""
if not self._active:
return
self._active = False
# 停止并销毁所有定时器
for t in self._timers:
t.stop()
t.deleteLater()
self._timers.clear()
# ON_EXIT 触发
on_exit = self.plan.entries_by_trigger(TriggerMoment.ON_EXIT)
if on_exit:
self._dispatch_entries(on_exit, label=f"seg{self.seg_index}/exit")
self.task.add_record(
"action_start",
f"离开航段[{self.seg_index}]ON_EXIT Actions 已触发",
)
logger.info(f"[Seg{self.seg_index}] 离开,定时器已清理")
# ── 核心分发逻辑 ──────────────────────────────────────────────
def _dispatch_entries(self, entries: List[ActionEntry], label: str = ""):
"""
按策略分发一组 ActionEntry
┌─────────────────────────────────────────────────────┐
│ 并行组order=None→ 同时提交到 ActionExecutor │
│ 串行组order!=None→ 提交到 SerialChainWorker │
│ serial_first 控制两组相对启动顺序 │
└─────────────────────────────────────────────────────┘
注意:串行组提交到线程池后立即返回(非阻塞),
线程池内部保证串行顺序,不阻塞主线程。
"""
parallel_entries = [e for e in entries if e.is_parallel and e.enabled]
sequential_entries = sorted(
[e for e in entries if e.is_sequential and e.enabled],
key=lambda e: e.order,
)
def _run_parallel():
for entry in parallel_entries:
self.action_executor.submit(entry.action)
logger.debug(
f"[并行/{label}] 提交 Action: {entry.action.type_key}"
)
def _run_sequential():
if not sequential_entries:
return
worker = _SerialChainWorker(
task_id = self.task.task_id,
entries = sequential_entries,
executor = self.action_executor,
label = label,
)
worker.signals.finished.connect(
lambda tid, res, si=self.seg_index:
self.serial_done.emit(tid, si, res)
)
worker.signals.error.connect(
lambda tid, err, si=self.seg_index:
self.serial_error.emit(tid, si, err)
)
self.pool.start(worker)
logger.debug(
f"[串行/{label}] 提交串行链,共 {len(sequential_entries)} 个 Action"
)
# 根据 serial_first 决定启动顺序
if self.plan.serial_first:
_run_sequential()
_run_parallel()
else:
_run_parallel()
_run_sequential()
def _dispatch_single(self, entry: ActionEntry):
"""分发单个 ActionEntryCONTINUOUS/SCHEDULED 回调)"""
if entry.enabled and self._active:
self.action_executor.submit(entry.action)
logger.debug(
f"[Seg{self.seg_index}] 触发 Action: {entry.action.type_key} "
f"trigger={entry.trigger.value}"
)
# ─────────────────────────────────────────────────────────────────
# 航线级 Action 调度器
# ─────────────────────────────────────────────────────────────────
class _RouteScheduler(QObject):
"""
管理整条航线级别的 Action 调度
负责ON_ENTER/ON_EXIT/CONTINUOUS/SCHEDULED 触发
"""
serial_done = pyqtSignal(str, list) # task_id, results
serial_error = pyqtSignal(str, str) # task_id, error
def __init__(
self,
task: Task,
action_executor,
thread_pool: QThreadPool,
parent: QObject = None,
):
super().__init__(parent)
self.task = task
self.action_executor = action_executor
self.pool = thread_pool
self._timers: List[QTimer] = []
self._active = False
def start(self):
"""航线开始时调用"""
self._active = True
plan = self.task.action_plan
# ON_ENTER 触发
on_enter = plan.route_entries_by_trigger(TriggerMoment.ON_ENTER)
if on_enter:
self._dispatch_entries(on_enter, plan)
# SCHEDULED 触发
for entry in plan.route_entries_by_trigger(TriggerMoment.SCHEDULED):
delay_ms = max(0, int(entry.time_offset_s * 1000))
t = QTimer(self)
t.setSingleShot(True)
t.timeout.connect(lambda e=entry: self._dispatch_single(e))
t.start(delay_ms)
self._timers.append(t)
# CONTINUOUS 触发
for entry in plan.route_entries_by_trigger(TriggerMoment.CONTINUOUS):
interval_ms = max(100, int(entry.interval_s * 1000))
t = QTimer(self)
t.timeout.connect(lambda e=entry: self._dispatch_single(e))
t.start(interval_ms)
self._timers.append(t)
logger.info(
f"[FlightRoute] 航线级调度启动 task={self.task.task_id[:8]} "
f"entries={len(plan.route_entries)}"
)
def stop(self):
"""航线结束时调用"""
if not self._active:
return
self._active = False
for t in self._timers:
t.stop()
t.deleteLater()
self._timers.clear()
# ON_EXIT 触发
plan = self.task.action_plan
on_exit = plan.route_entries_by_trigger(TriggerMoment.ON_EXIT)
if on_exit:
self._dispatch_entries(on_exit, plan)
logger.info(f"[FlightRoute] 航线级调度停止 task={self.task.task_id[:8]}")
def _dispatch_entries(self, entries: List[ActionEntry], plan: RouteActionPlan):
parallel_entries = [e for e in entries if e.is_parallel and e.enabled]
sequential_entries = sorted(
[e for e in entries if e.is_sequential and e.enabled],
key=lambda e: e.order,
)
def _run_parallel():
for entry in parallel_entries:
self.action_executor.submit(entry.action)
logger.debug(f"[并行/route] 提交: {entry.action.type_key}")
def _run_sequential():
if not sequential_entries:
return
worker = _SerialChainWorker(
task_id = self.task.task_id,
entries = sequential_entries,
executor = self.action_executor,
label = "route",
)
worker.signals.finished.connect(
lambda tid, res: self.serial_done.emit(tid, res)
)
worker.signals.error.connect(
lambda tid, err: self.serial_error.emit(tid, err)
)
self.pool.start(worker)
if plan.serial_first:
_run_sequential()
_run_parallel()
else:
_run_parallel()
_run_sequential()
def _dispatch_single(self, entry: ActionEntry):
if entry.enabled and self._active:
self.action_executor.submit(entry.action)
# ─────────────────────────────────────────────────────────────────
# Task 执行器主类
# ─────────────────────────────────────────────────────────────────
class TaskExecutor(QObject):
"""
Task 执行器(单例)
─────────────────────────────────────────────────────────────
使用方式:
executor = TaskExecutor.instance()
executor.set_action_executor(action_executor)
executor.submit(task)
─────────────────────────────────────────────────────────────
"""
_instance = None
_lock = threading.Lock()
# ── 对外信号 ──────────────────────────────────────────────────
task_submitted = pyqtSignal(object) # Task
task_started = pyqtSignal(object) # Task
task_completed = pyqtSignal(object) # Task
task_failed = pyqtSignal(object, str) # Task, error
task_cancelled = pyqtSignal(object) # Task
task_paused = pyqtSignal(object) # Task
task_resumed = pyqtSignal(object) # Task
segment_entered = pyqtSignal(object, int) # Task, seg_index
segment_exited = pyqtSignal(object, int) # Task, seg_index
action_triggered = pyqtSignal(object, object) # Task, ActionEntry
@classmethod
def instance(cls) -> "TaskExecutor":
with cls._lock:
if cls._instance is None:
cls._instance = cls()
return cls._instance
def __init__(self, max_threads: int = 8, parent=None):
super().__init__(parent)
self._pool = QThreadPool.globalInstance()
self._pool.setMaxThreadCount(max_threads)
self._action_executor = None
# 运行中的任务 { task_id: Task }
self._running: Dict[str, Task] = {}
self._run_lock = threading.Lock()
# 航线推进定时器 { task_id: QTimer }
self._route_timers: Dict[str, QTimer] = {}
# 航段调度器列表 { task_id: List[_SegmentScheduler | None] }
self._seg_schedulers: Dict[str, List] = {}
# 航线级调度器 { task_id: _RouteScheduler }
self._route_schedulers: Dict[str, _RouteScheduler] = {}
# # 当前活跃航段索引 { task_id: int }
# self._seg_cursor: Dict[str, int] = {}
# # 航段时间边界缓存 { task_id: [(t_start, t_end), ...] }
# self._seg_times: Dict[str, List[Tuple[float, float]]] = {}
# 任务实际启动时刻(用于时间轴对齐){ task_id: float }
# self._task_wall_start: Dict[str, float] = {}
logger.info(f"✅ TaskExecutor 初始化 max_threads={max_threads}")
@on_event(EventType.ROUTE_SEGMENT_EXITED,EventType.ROUTE_SEGMENT_ENTERED,EventType.ROUTE_WAYPOINT_REACHED)
def handle_route_event(self, event: Event):
segment_index = event.params.get("segment_index")
task = event.params.get("task")
self.start_segment_actions(event.event_type, segment_index, task)
def set_action_executor(self, executor) -> None:
"""注入 ActionExecutor"""
self._action_executor = executor
logger.info("✅ ActionExecutor 已注入 TaskExecutor")
# ── 提交接口 ──────────────────────────────────────────────────
def submit(self, task: Task) -> str:
"""
异步提交任务(立即返回)
:return: task_id
"""
if not self._action_executor:
raise RuntimeError("请先调用 set_action_executor() 注入 ActionExecutor")
task.mark_ready()
self.task_submitted.emit(task)
with self._run_lock:
self._running[task.task_id] = task
_task_procedure = functools.partial(self._start_task, task)
# 若有计划执行时间,延迟启动
if task.scheduled_at and task.scheduled_at > time.time():
delay_ms = int((task.scheduled_at - time.time()) * 1000)
QTimer.singleShot(delay_ms, _task_procedure)
logger.info(f"任务已调度,{delay_ms}ms 后启动: {task.name}")
else:
# 使用 singleShot(0) 确保在 Qt 事件循环中启动,避免递归
QTimer.singleShot(0, _task_procedure)
return task.task_id
def cancel(self, task_id: str) -> bool:
"""取消任务"""
with self._run_lock:
task = self._running.get(task_id)
if task:
task.request_cancel()
self._cleanup_task(task_id)
task.mark_cancelled()
self.task_cancelled.emit(task)
logger.info(f"🚫 任务已取消: {task.name}")
return True
return False
def pause(self, task_id: str) -> bool:
"""暂停任务(暂停航线推进定时器)"""
timer = self._route_timers.get(task_id)
task = self._running.get(task_id)
if timer and task and task.is_running:
timer.stop()
task.mark_paused()
self.task_paused.emit(task)
logger.info(f"⏸ 任务已暂停: {task.name}")
return True
return False
def resume(self, task_id: str) -> bool:
"""恢复任务"""
timer = self._route_timers.get(task_id)
task = self._running.get(task_id)
if timer and task and task.state == TaskState.PAUSED:
timer.start(100)
task.mark_resumed()
self.task_resumed.emit(task)
logger.info(f"▶ 任务已恢复: {task.name}")
return True
return False
# ── 内部:任务启动 ────────────────────────────────────────────
@pyqtSlot()
def _start_task(self, task: Task):
"""在 Qt 事件循环中启动任务"""
if task.cancel_requested:
return
task.mark_running()
# self._task_wall_start[task.task_id] = time.time()
self.task_started.emit(task)
logger.info(f"▶ 任务开始执行: {task.name} [{task.task_id[:8]}]")
# ── 启动航线级调度器 ──────────────────────────────────────
route_sched = _RouteScheduler(
task = task,
action_executor = self._action_executor,
thread_pool = self._pool,
parent = self,
)
route_sched.serial_done.connect(self._on_route_serial_done)
route_sched.serial_error.connect(self._on_route_serial_error)
self._route_schedulers[task.task_id] = route_sched
route_sched.start()
# ── 无航线任务:直接完成 ──────────────────────────────────
if not task.has_route:
self._execute_no_route_task(task)
return
# ── 缓存各航段时间边界(相对于任务启动时刻的偏移)────────
# seg_times: List[Tuple[float, float]] = []
# for seg in task.route.segments:
# # 航段时间 = 路由内部时间轴(相对于 route.start_time
# # 转换为相对于任务实际启动时刻的偏移
# offset = self._task_wall_start[task.task_id] - task.route.start_time
# seg_times.append((seg.t_start + offset, seg.t_end + offset))
# self._seg_times[task.task_id] = seg_times
# ── 初始化各航段调度器 ────────────────────────────────────
schedulers: List[Optional[_SegmentScheduler]] = []
for idx in range(len(task.route.segments)):
plan = task.action_plan.segment_plans.get(idx)
if plan:
sched = _SegmentScheduler(
task = task,
seg_index = idx,
plan = plan,
action_executor = self._action_executor,
thread_pool = self._pool,
parent = self,
)
sched.serial_done.connect(self._on_seg_serial_done)
sched.serial_error.connect(self._on_seg_serial_error)
else:
sched = None
schedulers.append(sched)
self._seg_schedulers[task.task_id] = schedulers
# self._seg_cursor[task.task_id] = -1 # -1 表示尚未进入任何航段
# ── 启动航线推进定时器100ms 轮询时间轴)────────────────
# timer = QTimer(self)
# _task_route = functools.partial(self._advance_route, task)
# timer.timeout.connect(_task_route)
# timer.start(100)
# self._route_timers[task.task_id] = timer
def _execute_no_route_task(self, task: Task):
"""
执行无航线任务:
直接调度所有 route_entries并行 + 串行),完成后结束任务
"""
plan = task.action_plan
# 并行组
for entry in plan.parallel_route_entries:
if entry.enabled:
self._action_executor.submit(entry.action)
# 串行组
seq_entries = plan.sequential_route_entries
if seq_entries:
worker = _SerialChainWorker(
task_id = task.task_id,
entries = seq_entries,
executor = self._action_executor,
label = "no_route",
)
worker.signals.finished.connect(
lambda tid, res: self._finish_task(task)
)
worker.signals.error.connect(
lambda tid, err: self._fail_task(task, err)
)
self._pool.start(worker)
else:
# 无串行组,等待并行 Action 提交后直接完成
QTimer.singleShot(500, lambda: self._finish_task(task))
# ── 内部:航线推进(时间轴轮询)──────────────────────────────
@pyqtSlot()
def start_segment_actions(self, event_type: EventType, seg_index: int, task: Task):
if task.cancel_requested or not task.is_running:
self._cleanup_task(task.task_id)
if task.cancel_requested:
task.mark_cancelled()
self.task_cancelled.emit(task)
return
# now = time.time()
# seg_times = self._seg_times.get(task.task_id, [])
schedulers = self._seg_schedulers.get(task.task_id, [])
# cur_idx = self._seg_cursor.get(task.task_id, -1)
# n_segs = len(seg_times)
#
# if not seg_times:
# return
#
## ── 判断当前时刻落在哪个航段 ──────────────────────────────
# new_idx = cur_idx
# for i, (t_start, t_end) in enumerate(seg_times):
# if t_start <= now <= t_end:
# new_idx = i
# break
# else:
# # 超出所有航段时间范围
# if now > seg_times[-1][1]:
# new_idx = n_segs # 标记为"已完成所有航段"
match event_type:
case EventType.ROUTE_SEGMENT_ENTERED:
if schedulers[seg_index]:
schedulers[seg_index].start()
self.segment_entered.emit(task, seg_index)
logger.info(
f"[FlightRoute] 进入航段[{seg_index}] task={task.task_id[:8]}"
)
case EventType.ROUTE_SEGMENT_EXITED:
if schedulers[seg_index]:
schedulers[seg_index].stop()
self.segment_exited.emit(task, seg_index)
logger.info(
f"[FlightRoute] 离开航段[{seg_index}] task={task.task_id[:8]}"
)
case EventType.ROUTE_WAYPOINT_REACHED:
pass
if seg_index == len(task.route.segments):
self._finish_task(task)
# # ── 航段切换处理 ──────────────────────────────────────────
# if new_idx != cur_idx:
# # 离开旧航段
# if 0 <= cur_idx < len(schedulers) and schedulers[cur_idx]:
# schedulers[cur_idx].stop()
# self.segment_exited.emit(task, cur_idx)
# logger.info(
# f"[FlightRoute] 离开航段[{cur_idx}] task={task.task_id[:8]}"
# )
#
# # 进入新航段
# if 0 <= new_idx < len(schedulers):
# if schedulers[new_idx]:
# schedulers[new_idx].start()
# self.segment_entered.emit(task, new_idx)
# logger.info(
# f"[FlightRoute] 进入航段[{new_idx}] task={task.task_id[:8]}"
# )
# self._seg_cursor[task.task_id] = new_idx
#
# # 所有航段执行完毕
# elif new_idx >= n_segs:
# self._seg_cursor[task.task_id] = new_idx
# self._finish_task(task)
@pyqtSlot()
def _advance_route(self, task: Task):
"""
每 100ms 轮询一次:
根据当前真实时刻与各航段时间边界,判断是否需要进入/离开航段
"""
if task.cancel_requested or not task.is_running:
self._cleanup_task(task.task_id)
if task.cancel_requested:
task.mark_cancelled()
self.task_cancelled.emit(task)
return
now = time.time()
seg_times = self._seg_times.get(task.task_id, [])
schedulers = self._seg_schedulers.get(task.task_id, [])
cur_idx = self._seg_cursor.get(task.task_id, -1)
n_segs = len(seg_times)
if not seg_times:
return
# ── 判断当前时刻落在哪个航段 ──────────────────────────────
new_idx = cur_idx
for i, (t_start, t_end) in enumerate(seg_times):
if t_start <= now <= t_end:
new_idx = i
break
else:
# 超出所有航段时间范围
if now > seg_times[-1][1]:
new_idx = n_segs # 标记为"已完成所有航段"
# ── 航段切换处理 ──────────────────────────────────────────
if new_idx != cur_idx:
# 离开旧航段
if 0 <= cur_idx < len(schedulers) and schedulers[cur_idx]:
schedulers[cur_idx].stop()
self.segment_exited.emit(task, cur_idx)
logger.info(
f"[FlightRoute] 离开航段[{cur_idx}] task={task.task_id[:8]}"
)
# 进入新航段
if 0 <= new_idx < len(schedulers):
if schedulers[new_idx]:
schedulers[new_idx].start()
self.segment_entered.emit(task, new_idx)
logger.info(
f"[FlightRoute] 进入航段[{new_idx}] task={task.task_id[:8]}"
)
self._seg_cursor[task.task_id] = new_idx
# 所有航段执行完毕
elif new_idx >= n_segs:
self._seg_cursor[task.task_id] = new_idx
self._finish_task(task)
# ── 内部:任务完成/失败 ───────────────────────────────────────
def _finish_task(self, task: Task):
"""正常完成任务"""
if task.is_done:
return
self._cleanup_task(task.task_id)
task.mark_completed()
self.task_completed.emit(task)
logger.info(f"✅ 任务完成: {task.name} 耗时={task.elapsed_s:.1f}s")
def _fail_task(self, task: Task, reason: str):
"""任务失败"""
if task.is_done:
return
self._cleanup_task(task.task_id)
task.mark_failed(reason)
self.task_failed.emit(task, reason)
logger.error(f"❌ 任务失败: {task.name} reason={reason}")
def _cleanup_task(self, task_id: str):
"""清理任务相关资源"""
# 停止航线推进定时器
timer = self._route_timers.pop(task_id, None)
if timer:
timer.stop()
timer.deleteLater()
# 停止所有航段调度器
for sched in self._seg_schedulers.pop(task_id, []):
if sched:
sched.stop()
# 停止航线级调度器
route_sched = self._route_schedulers.pop(task_id, None)
if route_sched:
route_sched.stop()
# 清理游标和时间缓存
self._seg_cursor.pop(task_id, None)
# self._seg_times.pop(task_id, None)
# self._task_wall_start.pop(task_id, None)
with self._run_lock:
self._running.pop(task_id, None)
# ── 槽函数 ────────────────────────────────────────────────────
@pyqtSlot(str, int, list)
def _on_seg_serial_done(self, task_id: str, seg_index: int, results: list):
logger.info(
f"[串行链] 航段[{seg_index}] 完成 "
f"results={len(results)} task={task_id[:8]}"
)
task = self._running.get(task_id)
if task:
task.add_record(
"action_done",
f"航段[{seg_index}] 串行 Action 链完成,共 {len(results)}",
{"seg_index": seg_index, "results_count": len(results)},
)
@pyqtSlot(str, int, str)
def _on_seg_serial_error(self, task_id: str, seg_index: int, error: str):
logger.error(f"[串行链] 航段[{seg_index}] 错误: {error}")
task = self._running.get(task_id)
if task:
task.add_record("error", f"航段[{seg_index}] 串行链错误: {error}")
@pyqtSlot(str, list)
def _on_route_serial_done(self, task_id: str, results: list):
logger.info(
f"[串行链] 航线级完成 results={len(results)} task={task_id[:8]}"
)
@pyqtSlot(str, str)
def _on_route_serial_error(self, task_id: str, error: str):
logger.error(f"[串行链] 航线级错误: {error}")
# ── 查询接口 ──────────────────────────────────────────────────
def get_running_tasks(self) -> List[Task]:
with self._run_lock:
return list(self._running.values())
def get_task(self, task_id: str) -> Optional[Task]:
with self._run_lock:
return self._running.get(task_id)
# 全局执行器实例
task_executor = TaskExecutor.instance()