"""入站传输管理器。 统一管理 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 AccountFilter, AccountStatus, 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 事件,根据 transport_mode 与适配器能力动态启动/停止对应账号的传输任务 (Task 11:pull 仅 Puller、stream 仅 Stream、both 优先 Stream 降级 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()], ) # 重启恢复:扫描 DB 中 ACTIVE 状态账号,为每个账号启动传输任务。 # 系统重启后不会收到 ChannelAccountOnline 事件,必须主动恢复, # 否则已上线账号的传输任务(SSE/短轮询)不会启动。 await self._restoreOnlineAccounts(trace_id) 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,根据 transport_mode 与适配器能力选择启动 Puller 或 Stream 任务(Task 11.1 + 11.2): - ``pull``: 仅启动 Puller - ``stream``: 仅启动 Stream - ``both``: 优先 Stream,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", trace_id=trace_id, channel_type=channel_type, account_id=account_id, ) await self._startTransportForAccount(channel_type, account_id, trace_id, source="event") async def _startTransportForAccount( self, channel_type: ChannelType, account_id: str, trace_id: str, source: str = "event", ) -> None: """为单个账号启动传输任务(事件驱动与重启恢复共用)。 根据 transport_mode 与适配器能力选择启动 Puller 或 Stream 任务: - ``pull``: 仅启动 Puller - ``stream``: 仅启动 Stream - ``both``: 优先 Stream,Puller 作为降级(Stream 适配器不可用时启动) 参数: channel_type: 渠道类型。 account_id: 账号 ID。 trace_id: 链路追踪 ID。 source: 启动来源(``event`` 事件驱动 / ``restore`` 重启恢复), 仅用于日志区分。 """ transport_mode = await self._resolveTransportMode(channel_type, account_id) puller_adapter = self._puller_registry.get(channel_type) stream_adapter = self._stream_connector_registry.get(channel_type) await self._logger.info( "starting transport for account", trace_id=trace_id, channel_type=channel_type, account_id=account_id, transport_mode=transport_mode, has_puller_adapter=puller_adapter is not None, has_stream_adapter=stream_adapter is not None, source=source, ) started = False if transport_mode == "pull": if puller_adapter is not None and self._puller_worker is not None: await self._puller_worker.start_account(channel_type, account_id, puller_adapter) started = True elif transport_mode == "stream": if stream_adapter is not None and self._stream_worker is not None: await self._stream_worker.start_account(channel_type, account_id, stream_adapter) started = True else: # Task 11.2: both 模式优先 Stream,Puller 作为降级。 # Stream 适配器可用时仅启动 Stream(Stream 健康时不 poll); # Stream 适配器不可用时降级启动 Puller。 if stream_adapter is not None and self._stream_worker is not None: await self._stream_worker.start_account(channel_type, account_id, stream_adapter) started = True elif puller_adapter is not None and self._puller_worker is not None: await self._puller_worker.start_account(channel_type, account_id, puller_adapter) started = True # 传输任务成功启动后持久化插件运行态(FR-32)。诊断字段,写入失败 # 仅告警不中止(与 last_health_check_at 一致的 best-effort 语义)。 if started: await self._touchPluginStatus(channel_type, account_id, "running", trace_id) async def _restoreOnlineAccounts(self, trace_id: str) -> None: """重启恢复:扫描 DB 中 ACTIVE 状态账号,启动传输任务。 系统重启后 TransportManager 不会收到 ChannelAccountOnline 事件, 本方法在 ``start()`` 中被调用,为所有已注册渠道的 ACTIVE 状态账号 启动传输任务(SSE 或短轮询),实现重启后自动恢复。 错误隔离:单个账号查询/启动失败不阻塞其他账号,仅记录 warn 日志。 仅恢复注册表中有 puller 或 stream 适配器的渠道(未注册适配器的 渠道跳过)。 参数: trace_id: 启动链路追踪 ID。 """ restored = 0 failed = 0 # 合并 puller 和 stream 注册表的渠道类型,避免遗漏 channel_types = set(self._puller_registry.keys()) | set(self._stream_connector_registry.keys()) for channel_type in channel_types: try: accounts = await self._persistence_port.findAccountsByFilter( AccountFilter(channel_type=channel_type, status=AccountStatus.ACTIVE) ) except Exception as exc: await self._logger.warn( "transport restore: failed to query accounts for channel", trace_id=trace_id, channel_type=channel_type, error=str(exc), ) continue for account in accounts: try: await self._startTransportForAccount( channel_type=channel_type, account_id=account.account_id, trace_id=trace_id, source="restore", ) restored += 1 except Exception as exc: failed += 1 await self._logger.warn( "transport restore: failed to start account", trace_id=trace_id, channel_type=channel_type, account_id=account.account_id, error=str(exc), ) await self._logger.info( "transport restore completed", trace_id=trace_id, restored_accounts=restored, failed_accounts=failed, channel_count=len(channel_types), ) async def _resolveTransportMode( self, channel_type: ChannelType, account_id: str, ) -> str: """解析账号的传输模式(Task 11.1)。 优先使用账号级 ``transport_mode``(``ChannelAccount.transport_mode``), 账号不存在或读取失败时回退到 manifest 声明的渠道级 ``transport_mode``, 均不可用时回退到 ``both``。 参数: channel_type: 渠道类型。 account_id: 渠道账号ID。 返回: 传输模式字符串(``pull`` / ``stream`` / ``both``)。 """ try: account = await self._persistence_port.getChannelAccount(channel_type, account_id) except Exception as exc: await self._logger.warn( "failed to get channel account for transport_mode resolution, fallback to manifest default", channel_type=channel_type, account_id=account_id, error=str(exc), ) account = None if account is not None: return account.transport_mode plugin = self._plugin_registry.getPluginByChannelType(channel_type) if plugin is not None: return plugin.manifest.transport_mode return "both" 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) # 传输任务停止后持久化插件运行态(FR-32)。 await self._touchPluginStatus(channel_type, account_id, "stopped", trace_id) 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: """处理传输错误事件。 记录日志并将账户插件运行态置为 ``error``(FR-32)。P1 阶段可用于 监控告警。 参数: event: TransportErrorOccurred 事件。 """ trace_id = event.trace_id or str(uuid.uuid4()) channel_type_raw = event.payload.get("channel_type") account_id = event.payload.get("account_id") await self._logger.warn( "transport error occurred", trace_id=trace_id, channel_type=channel_type_raw, account_id=account_id, error_category=event.payload.get("error_category"), error_code=event.payload.get("error_code"), ) # 持久化插件运行态为 error(FR-32)。仅当事件携带账户信息时写入。 if channel_type_raw is not None and account_id is not None: await self._touchPluginStatus( ChannelType(channel_type_raw), account_id, "error", trace_id ) async def _touchPluginStatus( self, channel_type: ChannelType, account_id: str, plugin_status: str, trace_id: str, ) -> None: """更新账户插件运行态(FR-32)。 传输任务 start/stop/error 后调用 ``PersistencePort.updatePluginStatus`` 持久化插件运行态(running / stopped / error)。诊断字段,best-effort 写入:失败时仅记录告警,不中止传输主流程(与 ``last_health_check_at`` 一致)。 """ try: await self._persistence_port.updatePluginStatus( channel_type, account_id, plugin_status ) except Exception as exc: await self._logger.warn( "failed to update plugin_status", trace_id=trace_id, channel_type=channel_type, account_id=account_id, plugin_status=plugin_status, error=str(exc), ) 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", "transport.stream_reconnect_backoff_ms", "transport.stream_reconnect_max_backoff_ms", } ) _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)