base_agent/uas/action/action_executor.py

398 lines
15 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 -*-
"""
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()