ForcePilot/backend/package/yuxi/channel/runtime/startup.py
Kris d52e9b518d feat(channel/runtime): 新增链路追踪与启动运行时工具集
新增trace_ctx实现链路ID上下文管理与日志注入,添加boot.py启动管理器、startup.py启动任务执行器、backoff.py退避策略工具以及trace.py启动链路追踪模块,完善channel运行时基础能力
2026-05-21 10:28:46 +08:00

110 lines
3.7 KiB
Python

from __future__ import annotations
import asyncio
import logging
import time
from dataclasses import dataclass, field
from typing import Any, Awaitable, Callable
from yuxi.channel.runtime.backoff import BackoffConfig
logger = logging.getLogger(__name__)
StartupTask = Callable[[], Awaitable[Any]]
@dataclass
class StartupTaskResult:
name: str
success: bool
duration_ms: float
error: str | None = None
skipped: bool = False
_StartupBackoffConfig = BackoffConfig(
base_delay=0.5,
max_delay=5.0,
exponent=1.5,
jitter=True,
jitter_factor=0.1,
max_retries=3,
)
class StartupTaskRunner:
"""启动任务执行器 — 支持超时、重试、并行执行与结果追踪。"""
def __init__(self, default_timeout: float = 30.0, max_retries: int = 2, backoff_config: BackoffConfig | None = None):
self.default_timeout = default_timeout
self.max_retries = max_retries
self._backoff_config = backoff_config or _StartupBackoffConfig
self._results: list[StartupTaskResult] = []
self._results_lock = asyncio.Lock()
async def run(
self,
task: StartupTask,
*,
name: str | None = None,
timeout: float | None = None,
retries: int | None = None,
) -> StartupTaskResult:
task_name = name or getattr(task, "__name__", "unknown")
timeout_val = timeout if timeout is not None else self.default_timeout
retry_count = retries if retries is not None else self.max_retries
start = time.time()
last_error: Exception | None = None
for attempt in range(retry_count + 1):
try:
await asyncio.wait_for(task(), timeout=timeout_val)
duration_ms = (time.time() - start) * 1000
result = StartupTaskResult(
name=task_name,
success=True,
duration_ms=duration_ms,
)
async with self._results_lock:
self._results.append(result)
logger.info("Startup task '%s' completed in %.2fms", task_name, duration_ms)
return result
except asyncio.TimeoutError:
last_error = asyncio.TimeoutError(f"Task '{task_name}' timed out after {timeout_val}s")
logger.warning("Startup task '%s' timed out (attempt %d/%d)", task_name, attempt + 1, retry_count + 1)
except Exception as e:
last_error = e
logger.exception("Startup task '%s' failed (attempt %d/%d)", task_name, attempt + 1, retry_count + 1)
if attempt < retry_count:
await asyncio.sleep(self._backoff_config.compute_delay(attempt))
duration_ms = (time.time() - start) * 1000
result = StartupTaskResult(
name=task_name,
success=False,
duration_ms=duration_ms,
error=str(last_error) if last_error else "Unknown error",
)
async with self._results_lock:
self._results.append(result)
return result
async def run_parallel(
self,
tasks: list[tuple[StartupTask, str]],
*,
timeout: float | None = None,
) -> list[StartupTaskResult]:
"""并行执行多个启动任务。"""
coros = [self.run(task, name=name, timeout=timeout) for task, name in tasks]
return await asyncio.gather(*coros, return_exceptions=True)
async def get_results(self) -> list[StartupTaskResult]:
async with self._results_lock:
return list(self._results)
async def clear_results(self) -> None:
async with self._results_lock:
self._results.clear()