ForcePilot/backend/package/yuxi/channels/application/transport/manager.py
Kris 00092c818e chore: 批量代码优化与规范完善
本次提交包含多项代码优化与规范修正:
1. 文档与注释优化:修正注释术语、补充注解与FR编号
2. 代码格式调整:统一空格、换行与缩进规范
3. 类型与接口完善:补充__all__导出、修正返回类型注解
4. 错误处理增强:新增领域错误类与校验逻辑
5. 依赖与导入调整:修复路径引用、统一时区导入
6. 协议与契约更新:完善接口文档与一致性注解
2026-07-03 19:18:13 +08:00

537 lines
19 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.

"""入站传输管理器。
统一管理 PullerWorker 和 StreamWorker 的生命周期,订阅账号上下线事件,
动态启动/停止账号的传输任务。
"""
from __future__ import annotations
import asyncio
import uuid
from collections.abc import Awaitable, Callable
from typing import Any
from yuxi.channels.application.transport.base_worker import (
TransportConfig,
)
from yuxi.channels.application.transport.puller_worker import PullerWorker
from yuxi.channels.application.transport.stream_worker import StreamWorker
from yuxi.channels.contract.dtos.channel import ChannelType
from yuxi.channels.contract.dtos.health import TransportHealthSnapshot
from yuxi.channels.contract.dtos.plugin import DomainEvent, EventHandler
from yuxi.channels.contract.plugin.extension_point import EventSubscription
from yuxi.channels.contract.plugin.manifest import FailurePolicy
from yuxi.channels.contract.ports.driven import (
ConfigPort,
LoggerPort,
PersistencePort,
)
from yuxi.channels.core.registry import PluginRegistry
__all__ = ["TransportManager"]
# 管理循环的轮询间隔(秒):仅用于等待取消信号,不参与业务逻辑
_MANAGER_LOOP_INTERVAL_S: float = 1.0
class TransportManager:
"""入站传输全局管理器。
单例模式管理 PullerWorker 和 StreamWorker订阅 ChannelAccountOnline/Offline
事件,动态启动/停止对应账号的传输任务。P0 阶段仅支持 Puller 模式。
职责:
- 从 PluginRegistry 动态构建 puller/stream 适配器注册表
- 创建并管理 PullerWorker、StreamWorker 单例
- 订阅账号上下线事件,驱动 Worker 启动/停止账号任务
- 提供全局健康状态查询
- 优雅关停所有传输任务
"""
def __init__(
self,
*,
plugin_registry: PluginRegistry,
persistence_port: PersistencePort,
config_port: ConfigPort,
event_bus: Any,
circuit_breaker: Any,
logger: LoggerPort,
message_deliverer: Callable[[Any], Awaitable[Any]],
transport_config: TransportConfig | None = None,
) -> None:
"""初始化 TransportManager。
参数:
plugin_registry: 插件注册表,用于动态获取渠道适配器。
persistence_port: 持久化端口,用于游标持久化等。
config_port: 配置端口。
event_bus: 事件总线,用于订阅/发布事件。
circuit_breaker: 熔断器实例。
logger: 日志端口。
message_deliverer: 入站消息投递回调。
transport_config: 传输配置,为 None 时使用默认配置。
"""
self._plugin_registry = plugin_registry
self._persistence_port = persistence_port
self._config_port = config_port
self._event_bus = event_bus
self._circuit_breaker = circuit_breaker
self._logger = logger
self._message_deliverer = message_deliverer
self._config = transport_config or TransportConfig()
self._puller_registry: dict[ChannelType, Any] = {}
self._stream_connector_registry: dict[ChannelType, Any] = {}
self._puller_worker: PullerWorker | None = None
self._stream_worker: StreamWorker | None = None
self._manager_task: asyncio.Task[None] | None = None
self._running: bool = False
self._plugin_id = "transport-manager"
async def start(self) -> None:
"""启动传输管理器。
- 构建 puller_registry 和 stream_connector_registry
- 创建 PullerWorker 和 StreamWorker 实例
- 启动 Worker
- 订阅账号上下线事件
- 启动管理循环任务
"""
if self._running:
return
trace_id = str(uuid.uuid4())
puller_registry: dict[ChannelType, Any] = {}
stream_connector_registry: dict[ChannelType, Any] = {}
for channel_type, plugin_da in self._plugin_registry.listPluginAdapters():
if plugin_da.puller_adapters:
puller_registry[channel_type] = plugin_da.puller_adapters[0]
if plugin_da.stream_connector_adapters:
stream_connector_registry[channel_type] = plugin_da.stream_connector_adapters[0]
self._puller_registry = puller_registry
self._stream_connector_registry = stream_connector_registry
self._puller_worker = PullerWorker(
message_deliverer=self._message_deliverer,
logger=self._logger,
config_port=self._config_port,
event_publisher=self._event_bus,
circuit_breaker=self._circuit_breaker,
persistence_port=self._persistence_port,
config=self._config,
)
self._stream_worker = StreamWorker(
message_deliverer=self._message_deliverer,
logger=self._logger,
config_port=self._config_port,
event_publisher=self._event_bus,
circuit_breaker=self._circuit_breaker,
config=self._config,
)
await self._puller_worker.start()
await self._stream_worker.start()
self._registerEventHandlers()
self._running = True
self._manager_task = asyncio.create_task(
self._manager_loop(),
name="transport-manager-loop",
)
await self._logger.info(
"transport manager started",
trace_id=trace_id,
puller_channels=[ct.value for ct in puller_registry.keys()],
stream_channels=[ct.value for ct in stream_connector_registry.keys()],
)
async def stop(self, timeout: float = 5.0) -> None:
"""停止传输管理器。
- 取消事件订阅
- 取消管理循环任务
- 停止所有 Worker
- 等待所有任务完成
参数:
timeout: 等待 Worker 停止的超时时间(秒)。
"""
if not self._running:
return
trace_id = str(uuid.uuid4())
self._running = False
self._unregisterEventHandlers()
if self._manager_task is not None and not self._manager_task.done():
self._manager_task.cancel()
try:
await self._manager_task
except asyncio.CancelledError:
pass
self._manager_task = None
if self._puller_worker is not None:
await self._puller_worker.stop(timeout)
self._puller_worker = None
if self._stream_worker is not None:
await self._stream_worker.stop(timeout)
self._stream_worker = None
await self._logger.info(
"transport manager stopped",
trace_id=trace_id,
)
def getHealth(self) -> TransportHealthSnapshot:
"""返回传输引擎健康状态(同步,向后兼容)。
返回:
``TransportHealthSnapshot``,包含 running 状态和各 Worker 健康
子状态(``WorkerHealthSnapshot``)。
"""
return TransportHealthSnapshot(
running=self._running,
puller=self._puller_worker.getHealth() if self._puller_worker else None,
stream=self._stream_worker.getHealth() if self._stream_worker else None,
)
async def getTransportHealth(self) -> TransportHealthSnapshot:
"""返回传输引擎健康状态(实现 TransportHealthPortFR-18
供 ``HealthAggregator`` 通过 ``TransportHealthPort`` 聚合,暴露每个
账号的运行状态running/backoff/stopped/error。实现幂等、无副作用
不抛异常阻塞调用方。
返回:
``TransportHealthSnapshot``,结构见 ``TransportHealthPort`` 协议
文档。异常时降级返回 ``puller`` / ``stream`` 为 ``None`` 的快照。
"""
try:
return self.getHealth()
except Exception as exc:
# 健康检查不得抛异常阻塞调用方(与 HealthAggregator 超时降级策略一致)
await self._logger.warn(
"transport health query failed, returning empty state",
error=str(exc),
)
return TransportHealthSnapshot(
running=self._running,
puller=None,
stream=None,
)
async def _manager_loop(self) -> None:
"""管理循环,等待取消信号。"""
try:
while self._running:
await asyncio.sleep(_MANAGER_LOOP_INTERVAL_S)
except asyncio.CancelledError:
pass
def _registerEventHandlers(self) -> None:
"""注册事件处理器。"""
self._event_bus.register(
self._plugin_id,
EventSubscription(
event_type="ChannelAccountOnline",
handler=_AccountEventHandler(self._on_account_online),
priority=100,
failure_policy=FailurePolicy.DEGRADE,
),
)
self._event_bus.register(
self._plugin_id,
EventSubscription(
event_type="ChannelAccountOffline",
handler=_AccountEventHandler(self._on_account_offline),
priority=100,
failure_policy=FailurePolicy.DEGRADE,
),
)
self._event_bus.register(
self._plugin_id,
EventSubscription(
event_type="ChannelDegraded",
handler=_AccountEventHandler(self._on_channel_degraded),
priority=100,
failure_policy=FailurePolicy.DEGRADE,
),
)
self._event_bus.register(
self._plugin_id,
EventSubscription(
event_type="ChannelRecovered",
handler=_AccountEventHandler(self._on_channel_recovered),
priority=100,
failure_policy=FailurePolicy.DEGRADE,
),
)
self._event_bus.register(
self._plugin_id,
EventSubscription(
event_type="TransportErrorOccurred",
handler=_AccountEventHandler(self._on_transport_error),
priority=100,
failure_policy=FailurePolicy.DEGRADE,
),
)
# 配置热更新订阅FR-04transport.* 配置变更时应用 hot/restart 策略
for config_event in ("ConfigChanged", "ConfigRollback"):
self._event_bus.register(
self._plugin_id,
EventSubscription(
event_type=config_event,
handler=_AccountEventHandler(self._on_config_changed),
priority=100,
failure_policy=FailurePolicy.DEGRADE,
),
)
def _unregisterEventHandlers(self) -> None:
"""取消事件订阅。"""
self._event_bus.unregister(self._plugin_id)
async def _on_account_online(self, event: DomainEvent) -> None:
"""处理账号上线事件。
从事件 payload 获取 channel_type 和 account_id查找适配器
启动对应账号的 Puller 和 Stream 任务。
参数:
event: ChannelAccountOnline 事件。
"""
trace_id = event.trace_id or str(uuid.uuid4())
channel_type_raw = event.payload.get("channel_type")
account_id = event.payload.get("account_id")
if channel_type_raw is None or account_id is None:
await self._logger.warn(
"account online event missing required fields, skip",
trace_id=trace_id,
channel_type=channel_type_raw,
account_id=account_id,
)
return
channel_type = ChannelType(channel_type_raw)
await self._logger.info(
"received account online event, starting transport",
trace_id=trace_id,
channel_type=channel_type.value,
account_id=account_id,
)
if self._puller_worker is not None:
adapter = self._puller_registry.get(channel_type)
if adapter is not None:
await self._puller_worker.start_account(channel_type, account_id, adapter)
if self._stream_worker is not None:
adapter = self._stream_connector_registry.get(channel_type)
if adapter is not None:
await self._stream_worker.start_account(channel_type, account_id, adapter)
async def _on_account_offline(self, event: DomainEvent) -> None:
"""处理账号下线事件。
停止对应账号的所有传输任务。
参数:
event: ChannelAccountOffline 事件。
"""
trace_id = event.trace_id or str(uuid.uuid4())
channel_type_raw = event.payload.get("channel_type")
account_id = event.payload.get("account_id")
if channel_type_raw is None or account_id is None:
await self._logger.warn(
"account offline event missing required fields, skip",
trace_id=trace_id,
channel_type=channel_type_raw,
account_id=account_id,
)
return
channel_type = ChannelType(channel_type_raw)
reason = event.payload.get("reason", "offline")
await self._logger.info(
"received account offline event, stopping transport",
trace_id=trace_id,
channel_type=channel_type.value,
account_id=account_id,
reason=reason,
)
if self._puller_worker is not None:
await self._puller_worker.stop_account(channel_type, account_id, reason)
if self._stream_worker is not None:
await self._stream_worker.stop_account(channel_type, account_id, reason)
async def _on_channel_degraded(self, event: DomainEvent) -> None:
"""处理渠道降级事件。
P0 阶段留空P1 阶段实现降级逻辑(如暂停非关键任务、调整轮询间隔等)。
参数:
event: ChannelDegraded 事件。
"""
pass
async def _on_channel_recovered(self, event: DomainEvent) -> None:
"""处理渠道恢复事件。
P0 阶段留空P1 阶段实现恢复逻辑。
参数:
event: ChannelRecovered 事件。
"""
pass
async def _on_transport_error(self, event: DomainEvent) -> None:
"""处理传输错误事件。
P0 阶段仅记录日志P1 阶段可用于监控告警。
参数:
event: TransportErrorOccurred 事件。
"""
await self._logger.warn(
"transport error occurred",
trace_id=event.trace_id,
channel_type=event.payload.get("channel_type"),
account_id=event.payload.get("account_id"),
error_category=event.payload.get("error_category"),
error_code=event.payload.get("error_code"),
)
async def _on_config_changed(self, event: DomainEvent) -> None:
"""处理配置变更事件FR-04 配置热更新)。
监听 ``transport.*`` 前缀的全局配置变更,按 hot/restart 模式应用:
- hot 模式(``stall_timeout_ms`` / ``backoff_schedule`` /
``backoff_jitter``):重新加载 TransportConfig 并更新 Worker 配置,
当前轮次结束后生效。
- restart 模式(``max_restart_attempts`` /
``graceful_shutdown_timeout_s``):记录警告日志,需手动重启
TransportManager 生效P1 阶段不实现自动重启,避免账号连接中断)。
参数:
event: ConfigChanged / ConfigRollback 事件payload 包含 key。
"""
key = event.payload.get("key", "")
if not key.startswith("transport."):
return
# 仅处理全局配置target 为 None 或空),忽略账户级配置
target = event.payload.get("target")
if target:
return
trace_id = event.trace_id or str(uuid.uuid4())
if key in _RESTART_REQUIRED_CONFIG_KEYS:
await self._logger.warn(
"transport config change requires restart to take effect",
trace_id=trace_id,
key=key,
event_type=event.event_type,
)
return
# 仅对 hot-reloadable 键触发重新加载,忽略未知 transport.* 键
if key not in _RELOADABLE_CONFIG_KEYS:
return
# hot 模式:重新加载配置并更新 Worker
await self._logger.info(
"transport config hot reload triggered",
trace_id=trace_id,
key=key,
event_type=event.event_type,
)
await self.reloadConfig()
async def reloadConfig(self) -> None:
"""重新加载传输配置并更新 WorkerFR-04 hot 模式)。
从 ``ConfigPort`` 读取最新 ``transport.*`` 配置,更新
``TransportManager._config`` 及两个 Worker 的 ``_config``。
Worker 在下一轮循环(退避等待、轮询间隔、心跳间隔)时自动应用新配置。
幂等:多次调用安全,仅更新配置不重启任务。
"""
trace_id = str(uuid.uuid4())
try:
new_config = await self._loadTransportConfig()
except Exception as exc:
await self._logger.error(
"transport config reload failed, keeping old config",
trace_id=trace_id,
error=str(exc),
)
return
self._config = new_config
if self._puller_worker is not None:
self._puller_worker._config = new_config
if self._stream_worker is not None:
self._stream_worker._config = new_config
await self._logger.info(
"transport config reloaded",
trace_id=trace_id,
stall_timeout_ms=new_config.stall_timeout_ms,
backoff_schedule=list(new_config.backoff_schedule),
backoff_jitter=new_config.backoff_jitter,
)
async def _loadTransportConfig(self) -> TransportConfig:
"""从 ConfigPort 加载 TransportConfig。
复用 BaseTransportWorker._loadConfig 的读取逻辑,集中配置加载
避免逻辑重复。
"""
# 委托任一 Worker 的 _loadConfig两者配置相同无 Worker 时
# 直接构造默认配置
if self._puller_worker is not None:
return await self._puller_worker._loadConfig()
if self._stream_worker is not None:
return await self._stream_worker._loadConfig()
return TransportConfig()
# transport.* 配置键的热更新模式分类FR-04
_RELOADABLE_CONFIG_KEYS: frozenset[str] = frozenset(
{
"transport.stall_timeout_ms",
"transport.backoff_schedule",
"transport.backoff_jitter",
}
)
_RESTART_REQUIRED_CONFIG_KEYS: frozenset[str] = frozenset(
{
"transport.max_restart_attempts",
"transport.graceful_shutdown_timeout_s",
}
)
class _AccountEventHandler(EventHandler):
"""账号事件处理器适配器。
将 EventHandler 接口适配到 TransportManager 的异步处理方法。
"""
def __init__(self, handler: Callable[[DomainEvent], Awaitable[None]]) -> None:
self._handler = handler
async def handle(self, event: DomainEvent) -> None:
await self._handler(event)