"""入站传输管理器。 统一管理 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 for ct in puller_registry.keys()], stream_channels=[ct 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: """返回传输引擎健康状态(实现 TransportHealthPort,FR-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-04):transport.* 配置变更时应用 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, 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, 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: """重新加载传输配置并更新 Worker(FR-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)