base_agent/uas/action/action_executor.py

398 lines
15 KiB
Python
Raw Normal View History

2026-06-18 03:28:14 +00:00
# -*- coding: utf-8 -*-
"""
Action 执行器
基于 PyQt5 QThreadPool + pyqtSignal 实现
- 异步提交 Action不阻塞主线程
- 处理函数产生的 Event 自动提交到 EventBus
- 超时检测QTimer 监控
- 优先级队列
- 执行历史记录
"""
from __future__ import annotations
import logging
import threading
import time
from collections import deque
from typing import Dict, List
from PyQt5.QtCore import (
QObject, QRunnable, QThreadPool,
pyqtSignal, pyqtSlot, QTimer
)
from uas.action.action_context import ActionContext, ActionCancelledError
from uas.action.action_model import Action, ActionResult, ActionState
from uas.action.action_type import action_registry
logger = logging.getLogger(__name__)
# ─────────────────────────────────────────────────────────────────
# Worker 信号载体
# ─────────────────────────────────────────────────────────────────
class _WorkerSignals(QObject):
finished = pyqtSignal(object) # Action
failed = pyqtSignal(object, str) # Action, error_msg
progress = pyqtSignal(str, int, str) # action_id, percent, message
events_ready = pyqtSignal(object, list) # Action, [Event, ...]
# ─────────────────────────────────────────────────────────────────
# Action Worker在 QThreadPool 工作线程中执行)
# ─────────────────────────────────────────────────────────────────
class _ActionWorker(QRunnable):
"""
QThreadPool 工作线程中执行单个 Action
执行完成后通过信号将产生的 Event 列表回传主线程
"""
def __init__(self, action: Action, shared_store: dict):
super().__init__()
self.action = action
self.shared_store = shared_store
self.signals = _WorkerSignals()
self.setAutoDelete(True)
@pyqtSlot()
def run(self):
action = self.action
action.mark_running()
# 构建执行上下文
ctx = ActionContext(
action = action,
shared_store = self.shared_store,
progress_cb = lambda aid, pct, msg:
self.signals.progress.emit(aid, pct, msg),
)
t0 = time.perf_counter()
try:
if action.handler is None:
raise ValueError(
f"Action [{action.type_key}] 未设置处理函数 handler"
)
# ── 执行处理函数 ──────────────────────────────────────
result_data = action.handler(action, ctx)
elapsed = (time.perf_counter() - t0) * 1000
# 收集处理函数产生的 Event
events = ctx.collected_events
result = ActionResult(
action_id = action.action_id,
action_type = action.type_key,
success = True,
data = result_data,
elapsed_ms = elapsed,
events = events,
)
action.mark_completed(result)
# 先发射 events_ready再发射 finished
if events:
self.signals.events_ready.emit(action, events)
self.signals.finished.emit(action)
logger.debug(
f"Action 完成: {action} elapsed={elapsed:.1f}ms "
f"events={len(events)}"
)
except ActionCancelledError as e:
action.mark_cancelled()
self.signals.finished.emit(action)
logger.info(f"Action 已取消: {action}")
except Exception as e:
elapsed = (time.perf_counter() - t0) * 1000
action.mark_failed(str(e))
self.signals.failed.emit(action, str(e))
logger.error(f"Action 执行失败: {action} error={e}", exc_info=True)
# ─────────────────────────────────────────────────────────────────
# Action 执行器主类
# ─────────────────────────────────────────────────────────────────
class ActionExecutor(QObject):
"""
Action 执行器单例
使用方式
executor = ActionExecutor.instance()
# 异步提交
executor.submit(my_action)
# 同步提交(阻塞等待)
result = executor.submit_sync(my_action)
# 取消
executor.cancel(action_id)
"""
_instance = None
_lock = threading.Lock()
# ── 对外信号 ──────────────────────────────────────────────────
action_submitted = pyqtSignal(object) # Action
action_started = pyqtSignal(object) # Action
action_completed = pyqtSignal(object) # Action
action_failed = pyqtSignal(object, str) # Action, error
action_cancelled = pyqtSignal(object) # Action
action_progress = pyqtSignal(str, int, str) # id, percent, msg
events_produced = pyqtSignal(object, list) # Action, [Event]
stats_updated = pyqtSignal(dict) # 统计字典
@classmethod
def instance(cls) -> "ActionExecutor":
with cls._lock:
if cls._instance is None:
cls._instance = cls()
return cls._instance
def __init__(self, max_threads: int = 8, history_size: int = 200, parent=None):
super().__init__(parent)
self._pool: QThreadPool = QThreadPool.globalInstance()
self._pool.setMaxThreadCount(max_threads)
# 运行中的 Action { action_id: Action }
self._running: Dict[str, Action] = {}
self._running_lock = threading.Lock()
# 超时监控定时器 { action_id: QTimer }
self._timers: Dict[str, QTimer] = {}
# 历史记录
self._history: deque[Action] = deque(maxlen=history_size)
# 共享数据存储(跨 Action 共享)
self._shared_store: dict = {}
# 统计
self._stats = {
"submitted": 0, "completed": 0,
"failed": 0, "cancelled": 0
}
# EventBus 引用(延迟导入避免循环依赖)
self._event_bus = None
logger.info(f"✅ ActionExecutor 初始化 max_threads={max_threads}")
def set_event_bus(self, bus) -> None:
"""注入 EventBus用于自动提交 Action 产生的 Event"""
self._event_bus = bus
# 连接 events_produced 信号到自动提交槽
self.events_produced.connect(self._auto_post_events)
logger.info("✅ EventBus 已注入 ActionExecutor")
# ── 提交接口 ──────────────────────────────────────────────────
def submit(self, action: Action) -> str:
"""
异步提交 Action立即返回不阻塞主线程
:return: action_id
"""
action.mark_queued()
self._stats["submitted"] += 1
self._add_history(action)
self.action_submitted.emit(action)
# 注册超时监控
self._start_timeout_timer(action)
# 提交到线程池
worker = _ActionWorker(action, self._shared_store)
worker.signals.finished.connect(self._on_worker_finished)
worker.signals.failed.connect(self._on_worker_failed)
worker.signals.progress.connect(self._on_worker_progress)
worker.signals.events_ready.connect(self._on_events_ready)
with self._running_lock:
self._running[action.action_id] = action
self._pool.start(worker)
logger.debug(f"异步提交 Action: {action}")
return action.action_id
def submit_sync(self, action: Action, timeout_sec: float = 30.0) -> ActionResult:
"""
同步提交 Action阻塞等待完成
:return: ActionResult
"""
action.mark_queued()
action.mark_running()
self._stats["submitted"] += 1
self._add_history(action)
ctx = ActionContext(action=action, shared_store=self._shared_store)
t0 = time.perf_counter()
try:
if action.handler is None:
raise ValueError(f"Action [{action.type_key}] 未设置 handler")
result_data = action.handler(action, ctx)
elapsed = (time.perf_counter() - t0) * 1000
events = ctx.collected_events
result = ActionResult(
action_id = action.action_id,
action_type = action.type_key,
success = True,
data = result_data,
elapsed_ms = elapsed,
events = events,
)
action.mark_completed(result)
self._stats["completed"] += 1
# 同步模式下直接提交 Event
if self._event_bus and events:
for event in events:
self._event_bus.post(event)
self.action_completed.emit(action)
self._emit_stats()
return result
except ActionCancelledError:
action.mark_cancelled()
self._stats["cancelled"] += 1
self.action_cancelled.emit(action)
return ActionResult(
action_id=action.action_id, action_type=action.type_key,
success=False, error="已取消"
)
except Exception as e:
action.mark_failed(str(e))
self._stats["failed"] += 1
self.action_failed.emit(action, str(e))
return action.result
def cancel(self, action_id: str) -> bool:
"""取消指定 Action"""
with self._running_lock:
action = self._running.get(action_id)
if action:
return action.request_cancel()
return False
def cancel_all(self) -> int:
"""取消所有运行中的 Action"""
with self._running_lock:
actions = list(self._running.values())
count = sum(1 for a in actions if a.request_cancel())
logger.info(f"取消 {count} 个 Action")
return count
# ── 超时监控 ──────────────────────────────────────────────────
def _start_timeout_timer(self, action: Action):
meta = action_registry.get(action.type_key)
if not meta:
return
timeout_ms = int(meta.timeout_sec * 1000)
timer = QTimer(self)
timer.setSingleShot(True)
timer.timeout.connect(lambda: self._on_timeout(action.action_id))
timer.start(timeout_ms)
self._timers[action.action_id] = timer
def _stop_timeout_timer(self, action_id: str):
timer = self._timers.pop(action_id, None)
if timer:
timer.stop()
timer.deleteLater()
@pyqtSlot()
def _on_timeout(self, action_id: str = None):
with self._running_lock:
action = self._running.get(action_id)
if action and action.is_running:
action.mark_timeout()
action.request_cancel()
logger.warning(f"⏱️ Action 超时: {action}")
self.action_failed.emit(action, "执行超时")
# ── Worker 槽函数 ─────────────────────────────────────────────
@pyqtSlot(object)
def _on_worker_finished(self, action: object):
self._stop_timeout_timer(action.action_id)
with self._running_lock:
self._running.pop(action.action_id, None)
if action.state == ActionState.CANCELLED:
self._stats["cancelled"] += 1
self.action_cancelled.emit(action)
else:
self._stats["completed"] += 1
self.action_completed.emit(action)
self._emit_stats()
logger.debug(f"Action 完成回调: {action}")
@pyqtSlot(object, str)
def _on_worker_failed(self, action: object, error: str):
self._stop_timeout_timer(action.action_id)
with self._running_lock:
self._running.pop(action.action_id, None)
self._stats["failed"] += 1
self.action_failed.emit(action, error)
self._emit_stats()
@pyqtSlot(str, int, str)
def _on_worker_progress(self, action_id: str, percent: int, message: str):
self.action_progress.emit(action_id, percent, message)
@pyqtSlot(object, list)
def _on_events_ready(self, action: object, events: list):
self.events_produced.emit(action, events)
@pyqtSlot(object, list)
def _auto_post_events(self, action: object, events: list):
"""自动将 Action 产生的 Event 提交到 EventBus"""
if self._event_bus:
for event in events:
self._event_bus.post(event)
logger.debug(
f"自动提交 Event: {event} <- Action[{action.type_key}]"
)
# ── 内部工具 ──────────────────────────────────────────────────
def _add_history(self, action: Action):
self._history.append(action)
def _emit_stats(self):
self.stats_updated.emit(dict(self._stats))
# ── 查询接口 ──────────────────────────────────────────────────
def get_history(self, limit: int = 50) -> List[Action]:
return list(self._history)[-limit:]
def get_running(self) -> List[Action]:
with self._running_lock:
return list(self._running.values())
def get_stats(self) -> dict:
return dict(self._stats)
def wait_for_done(self, timeout_ms: int = 10000):
self._pool.waitForDone(timeout_ms)
# 全局执行器实例
action_executor = ActionExecutor.instance()