fish_async_task.worker.executor 源代码

"""
任务执行器模块

负责任务的实际执行,包括超时控制和状态更新。
"""

import logging
import queue
import threading
import time
from collections import deque
from typing import Any, Callable, Deque, Dict, List, Optional, Tuple

from ..types import TaskStatus, TaskTuple


[文档] class TaskExecutor: """ 任务执行器 负责任务的实际执行,包括超时控制和状态更新。 每个任务在独立的工作线程中执行,支持超时机制。 线程安全说明: - execute_task方法可以在多个线程中并发调用 - 任务执行是独立的,不会相互影响 - 超时机制使用daemon线程实现,超时后任务线程仍在后台运行 - 通过 cleanup_timed_out_tasks 方法可以清理超时的任务线程引用 """ # 超时任务跟踪相关常量 MAX_TRACKED_TIMEOUTS = 1000 # 最多跟踪的超时任务数量 TIMEOUT_TASK_EXPIRY = 3600 # 超时任务信息过期时间(秒) # 批量状态更新相关常量 BATCH_SIZE = 100 # 批量更新大小 BATCH_FLUSH_INTERVAL = 0.1 # 批量刷新间隔(秒)
[文档] def __init__( self, logger: logging.Logger, task_timeout_getter: Callable[[], Optional[float]], update_status_func: Callable[..., None], batch_size: Optional[int] = None, batch_flush_interval: Optional[float] = None, ): """ 初始化任务执行器 Args: logger: 日志记录器 task_timeout_getter: 获取任务超时时间的函数(支持动态获取) update_status_func: 状态更新函数 batch_size: 批量更新大小(可选,默认使用类常量) batch_flush_interval: 批量刷新间隔(秒)(可选,默认使用类常量) """ self.logger = logger self._get_task_timeout = task_timeout_getter self._update_status = update_status_func # 批量状态更新配置 self._batch_size = batch_size if batch_size is not None else self.BATCH_SIZE self._batch_flush_interval = ( batch_flush_interval if batch_flush_interval is not None else self.BATCH_FLUSH_INTERVAL ) # 批量状态更新队列 self._batch_updates: deque = deque() self._batch_lock = threading.Lock() self._last_batch_flush = time.time() # 超时任务跟踪(用于资源清理) self._timed_out_tasks: Dict[str, float] = {} self._timed_out_tasks_lock = threading.Lock() # 清理回调列表(用于自定义清理逻辑) self._cleanup_callbacks: List[Callable[[str], None]] = []
def _queue_status_update( self, task_id: str, status: TaskStatus, start_time: Optional[float] = None, end_time: Optional[float] = None, result: Any = None, error: Optional[str] = None, ) -> None: """ 将状态更新添加到批量队列 Args: task_id: 任务ID status: 任务状态 start_time: 任务开始时间(可选) end_time: 任务结束时间(可选) result: 任务执行结果(可选) error: 错误信息(可选) """ update_item = (task_id, status, start_time, end_time, result, error) with self._batch_lock: self._batch_updates.append(update_item) # 检查是否需要刷新 current_time = time.time() time_since_last_flush = current_time - self._last_batch_flush # 如果批量大小达到阈值或超过刷新间隔,立即刷新 if ( len(self._batch_updates) >= self._batch_size or time_since_last_flush >= self._batch_flush_interval ): self._last_batch_flush = current_time self._flush_batch_updates() def _flush_batch_updates(self) -> None: """刷新批量更新(需要在 batch_lock 保护下调用)""" if not self._batch_updates: return # 获取批量数据(在锁保护下) batch_data = list(self._batch_updates) self._batch_updates.clear() # 释放锁后执行更新 # 注意:这里假设调用者已经持有锁 try: for task_id, status, start_time, end_time, result, error in batch_data: try: self._update_status( task_id, status, start_time=start_time, end_time=end_time, result=result, error=error, ) except Exception as e: self.logger.error( f"批量更新任务状态失败 [{type(e).__name__}]: {e}", exc_info=True ) except Exception: # 如果执行更新出错,重新将数据放回队列 with self._batch_lock: self._batch_updates.extend(batch_data) raise def _check_and_flush_batch(self) -> None: """检查是否需要刷新批量更新(基于时间间隔)""" with self._batch_lock: if not self._batch_updates: return if time.time() - self._last_batch_flush >= self._batch_flush_interval: self._flush_batch_updates()
[文档] def flush_pending_updates(self) -> int: """ 强制刷新所有待处理的批量更新 Returns: int: 刷新前队列中的更新数量 """ with self._batch_lock: pending_count = len(self._batch_updates) if pending_count > 0: self._flush_batch_updates() return pending_count
[文档] def get_pending_update_count(self) -> int: """ 获取当前待处理的批量更新数量 Returns: int: 待处理的更新数量 """ with self._batch_lock: return len(self._batch_updates)
[文档] def execute_task(self, task: TaskTuple) -> None: """ 执行任务 Args: task: 任务元组,包含(task_id, func, args, kwargs) """ task_id, func, args, kwargs = task start_time = time.time() try: # 更新任务状态为running(使用批量队列) self._queue_status_update(task_id, "running", start_time=start_time) self.logger.debug("任务 %s 开始执行", task_id) # 执行任务(支持超时) result = self._execute_with_timeout(task_id, func, args, kwargs) # 更新任务状态为completed(使用批量队列) end_time = time.time() self._queue_status_update( task_id, "completed", start_time=start_time, end_time=end_time, result=result ) self.logger.debug("任务 %s 执行完成,耗时 %.2f秒", task_id, end_time - start_time) except KeyboardInterrupt: # 键盘中断,标记为失败 end_time = time.time() self._queue_status_update( task_id, "failed", start_time=start_time, end_time=end_time, error="任务被中断" ) self.logger.warning(f"任务 {task_id} 被中断") raise except SystemExit: # 系统退出,重新抛出 raise except TimeoutError as e: # 超时异常,标记为失败 end_time = time.time() self._queue_status_update( task_id, "failed", start_time=start_time, end_time=end_time, error=str(e) ) self.logger.warning(f"任务 {task_id} 执行超时: {e}") except (IOError, OSError, ConnectionError) as e: # I/O 相关异常,可能是临时性问题 end_time = time.time() self._queue_status_update( task_id, "failed", start_time=start_time, end_time=end_time, error=str(e) ) self.logger.warning(f"任务 {task_id} 遇到 I/O 异常 [{type(e).__name__}]: {e}") except (MemoryError, ResourceWarning) as e: # 资源相关异常 end_time = time.time() self._queue_status_update( task_id, "failed", start_time=start_time, end_time=end_time, error=str(e) ) self.logger.error( f"任务 {task_id} 遇到资源异常 [{type(e).__name__}]: {e}", exc_info=True ) except Exception as e: # 记录任务执行异常 end_time = time.time() self._queue_status_update( task_id, "failed", start_time=start_time, end_time=end_time, error=str(e) ) error_type = type(e).__name__ self.logger.error(f"任务 {task_id} 执行失败 [{error_type}]: {e}", exc_info=True) finally: # 尝试刷新批量更新(基于时间间隔) self._check_and_flush_batch()
def _execute_with_timeout( self, task_id: str, func: Callable[..., Any], args: Tuple[Any, ...], kwargs: Dict[str, Any], ) -> Any: """ 执行任务,支持超时控制 Args: task_id: 任务ID func: 要执行的任务函数 args: 任务函数的 positional 参数 kwargs: 任务函数的关键字参数 Returns: Any: 任务执行结果 Raises: TimeoutError: 任务执行超时 Exception: 任务执行过程中的异常 Note: Python的线程无法被强制终止,超时后任务线程仍会继续运行。 这是Python GIL的限制,超时只是停止等待结果,但任务本身可能仍在执行。 如果需要真正的任务取消,考虑使用进程池或支持取消的第三方库。 Warning: 当任务超时时,虽然会抛出TimeoutError,但任务线程仍在后台运行。 这可能导致资源泄漏(文件句柄、网络连接等)。建议任务函数内部 实现超时检查机制,或使用支持取消的异步框架。 资源泄漏预防建议: 1. 在任务函数中使用上下文管理器(with语句)管理资源 2. 在任务函数中定期检查超时标志 3. 使用支持取消的异步框架(如 asyncio) 4. 对于长时间运行的任务,考虑使用进程池而非线程池 """ task_timeout = self._get_task_timeout() if not task_timeout: return func(*args, **kwargs) result_container = self._create_result_container() task_thread = self._create_and_start_task_thread(func, args, kwargs, result_container) self._wait_for_task_completion(task_thread, task_timeout, task_id, result_container) if result_container["exception"]: raise result_container["exception"] return result_container["result"] def _create_result_container(self) -> Dict[str, Any]: """创建结果容器""" return { "result": None, "exception": None, "completed": False, } def _create_and_start_task_thread( self, func: Callable[..., Any], args: Tuple[Any, ...], kwargs: Dict[str, Any], result_container: Dict[str, Any], ) -> threading.Thread: """创建并启动任务线程""" def task_wrapper() -> None: """任务包装器,捕获执行结果和异常""" try: result_container["result"] = func(*args, **kwargs) except Exception as e: result_container["exception"] = e finally: result_container["completed"] = True task_thread = threading.Thread(target=task_wrapper, daemon=True) task_thread.start() return task_thread def _wait_for_task_completion( self, task_thread: threading.Thread, task_timeout: float, task_id: str, result_container: Dict[str, Any], ) -> None: """等待任务完成或超时""" task_thread.join(timeout=task_timeout) if not result_container["completed"]: self._handle_task_timeout(task_id, task_timeout) def _handle_task_timeout(self, task_id: str, task_timeout: float) -> None: """处理任务超时""" self.logger.warning( f"任务 {task_id} 执行超时({task_timeout}秒)," f"但任务线程仍在后台运行,可能导致资源泄漏" ) self._track_timed_out_task(task_id) self._trigger_cleanup_callbacks(task_id) raise TimeoutError(f"任务 {task_id} 执行超时({task_timeout}秒)") def _track_timed_out_task(self, task_id: str) -> None: """跟踪超时任务""" current_time = time.time() with self._timed_out_tasks_lock: # 清理过期的超时任务记录 expired_tasks = [ tid for tid, expiry in self._timed_out_tasks.items() if current_time - expiry > self.TIMEOUT_TASK_EXPIRY ] for tid in expired_tasks: self._timed_out_tasks.pop(tid, None) # 添加新的超时任务记录 if len(self._timed_out_tasks) < self.MAX_TRACKED_TIMEOUTS: self._timed_out_tasks[task_id] = current_time
[文档] def add_cleanup_callback(self, callback: Callable[[str], None]) -> None: """ 添加清理回调函数 当任务超时时,会调用所有注册的清理回调函数。 Args: callback: 清理回调函数,接收任务ID作为参数 """ with self._timed_out_tasks_lock: self._cleanup_callbacks.append(callback)
def _trigger_cleanup_callbacks(self, task_id: str) -> None: """ 触发所有清理回调 Args: task_id: 超时的任务ID """ for callback in self._cleanup_callbacks: try: callback(task_id) except Exception as e: self.logger.error(f"清理回调执行失败 [{type(e).__name__}]: {e}", exc_info=True)
[文档] def cleanup_timed_out_tasks(self, max_cleanup: int = 100) -> int: """ 清理超时的任务线程引用 注意:由于Python线程无法被强制终止,此方法主要用于清理跟踪信息, 实际的任务线程仍会在后台运行直到任务完成或进程退出。 Args: max_cleanup: 最大清理数量 Returns: int: 清理的任务数量 """ current_time = time.time() cleaned_count = 0 with self._timed_out_tasks_lock: expired_tasks = [ (tid, expiry) for tid, expiry in self._timed_out_tasks.items() if current_time - expiry > self.TIMEOUT_TASK_EXPIRY ] # 限制每次清理的数量 for task_id, _ in expired_tasks[:max_cleanup]: self._timed_out_tasks.pop(task_id, None) cleaned_count += 1 if cleaned_count > 0: self.logger.info(f"清理了 {cleaned_count} 个超时任务跟踪记录") return cleaned_count
[文档] def get_timed_out_task_count(self) -> int: """ 获取当前跟踪的超时任务数量 Returns: int: 超时任务数量 """ with self._timed_out_tasks_lock: return len(self._timed_out_tasks)