"""宿主启动编排器。 本模块是编排层的启动编排入口,按顺序装配渠道网关的所有 组件。关键步骤失败必须终止启动并报告原因(INV-I5), 插件加载失败不得终止启动,必须触发优雅降级。 启动顺序不可颠倒: 1. 加载渠道配置(INF-009:配置缺失或类型不符必须抛异常) 2. ``ensure_channel_schema()`` 初始化渠道 schema(INV-I5:失败必须抛异常) 3. 初始化注册中心 + EventBus + 领域服务(注册到 DI 容器) 4. 加载并初始化被驱动适配器(INF-010:应用级适配器连通性检查) 5. 加载并初始化渠道插件(按依赖拓扑,失败记录警告继续) 6. 装配管道(INF-011:注入锚点格式校验) 7. 注册驱动适配器(开始接受流量) 8. 标记宿主为 ready(INF-012:与步骤 7 紧邻,对齐规范 §15.1, 后台任务启动置于 ready 之后) 9. 启动失败插件自动重载(FR-36) 10. 启动传输引擎管理器(入站Worker) 注:Outbox 恢复扫描(FR-22)、配对过期扫描(FR-33)与审计日志保留期 清理(FR-34)已迁移至 scheduler worker 的 handler,不再由 api 进程后台 任务执行,避免共享 ``AsyncSession`` 并发使用与锁 TTL 不匹配问题。 """ from __future__ import annotations import asyncio from collections.abc import Awaitable, Callable, Coroutine, Sequence from typing import Any, Protocol from yuxi.channels.application.extension.config_source_registry import ( ConfigSourceRegistry, ) from yuxi.channels.application.extension.event_bus import EventBus from yuxi.channels.application.extension.event_subscription_registry import ( EventSubscriptionRegistry, ) from yuxi.channels.application.extension.stage_slot_registry import StageSlotRegistry from yuxi.channels.application.health.health_aggregator import HealthAggregator from yuxi.channels.application.lifecycle.config_scope_registry import ( ConfigScopeRegistry, ) from yuxi.channels.application.lifecycle.plugin_dependency_resolver import ( PluginDependencyResolver, ) from yuxi.channels.application.lifecycle.plugin_lifecycle_manager import ( PluginLifecycleManager, ) from yuxi.channels.application.lifecycle.route_binding_loader import ( RouteMatchRegistryLoader, ) from yuxi.channels.application.lifecycle.sensitive_field_registry import ( SensitiveFieldRegistry, ) from yuxi.channels.application.pipeline.stage_slot_injector import StageSlotInjector from yuxi.channels.application.transport import TransportManager from yuxi.channels.contract.dtos.lifecycle import LifecycleResult from yuxi.channels.contract.dtos.plugin import DomainEvent from yuxi.channels.contract.dtos.route_events import ROUTE_BINDING_CHANGED from yuxi.channels.contract.errors import ( ConfigValidationError, DependencyError, Error, InternalError, ) from yuxi.channels.contract.errors.server import OperationTimeoutError from yuxi.channels.contract.plugin.extension_point import EventSubscription from yuxi.channels.contract.plugin.manifest import ChannelManifest, FailurePolicy from yuxi.channels.contract.ports.driven.config_port import ConfigPort from yuxi.channels.contract.ports.driven.event_publisher_port import EventPublisherPort from yuxi.channels.contract.ports.driven.logger_port import LoggerPort from yuxi.channels.contract.ports.driven.persistence_port import PersistencePort from yuxi.channels.contract.ports.driven.pingable_port import PingablePort from yuxi.channels.core.event.config import configureSensitiveFieldRegistry from yuxi.channels.core.registry.command_registry import CommandRegistryTable from yuxi.channels.core.registry.plugin_registry import PluginRegistry from yuxi.channels.core.registry.route_match_registry import RouteMatchRegistry from yuxi.channels.core.service.common_command_adapter import registerCommonCommands from yuxi.channels.infrastructure.dependency_injection import ( DependencyInjectionContainer, ) __all__ = [ "HostBootstrap", ] class _EnsureSchemaPort(Protocol): """渠道 schema 初始化端口(局部 Protocol)。 ``PostgresManager`` 满足此 Protocol(提供 ``ensure_channel_schema`` 方法)。 使用局部 Protocol 避免在 contract 层新增端口;``PostgresManager`` 位于 storage 层,不属于 channels 四层架构,直接依赖会破坏分层边界。 """ async def ensure_channel_schema(self) -> None: """确保渠道网关 schema 就绪。""" ... class _RouteBindingChangedHandler: """``RouteBindingChanged`` 领域事件处理器。 订阅 ``RouteBindingChanged`` 事件,调用 ``RouteMatchRegistryLoader.reloadAccount`` 增量刷新指定账户的路由绑定规则。作为内置订阅者在 ``HostBootstrap._loadRouteBindingsFromDB`` 中注册到 EventBus。 handler 异常由 ``EventBus.publish`` 捕获并记录警告日志,不中断其他 订阅者(FR-36),故本处理器不添加 try/except 防御。 """ def __init__(self, loader: RouteMatchRegistryLoader) -> None: self._loader = loader async def handle(self, event: DomainEvent) -> None: """处理 ``RouteBindingChanged`` 事件,刷新对应账户的路由绑定。 从事件 payload 提取 ``channel_type`` 与 ``account_id``,调用 ``loader.reloadAccount`` 增量刷新注册表。payload 缺少必要字段时 静默跳过(事件发布方契约保证字段完整,此处防御性检查)。 """ payload = event.payload channel_type = payload.get("channel_type") account_id = payload.get("account_id") if not channel_type or not account_id: return await self._loader.reloadAccount(str(channel_type), str(account_id)) class HostBootstrap: """宿主启动编排器。 按顺序装配渠道网关所有组件(INV-I5)。 启动顺序不可颠倒: 1. 加载配置 2. ``ensure_channel_schema()`` 初始化渠道 schema 3. 初始化注册中心 + EventBus + 领域服务 4. 加载并初始化被驱动适配器 5. 加载路由绑定规则到 ``RouteMatchRegistry``(DB → 内存注册表) 6. 加载并初始化渠道插件(按依赖拓扑) 7. 装配管道(注入插件阶段) 8. 注册驱动适配器(开始接受流量) 9. 标记宿主为 ready(INF-012:与步骤 8 紧邻,对齐规范 §15.1) 10. 启动失败插件自动重载(FR-36) 11. 启动传输引擎管理器 关键约束: - 关键步骤(配置加载、Schema 初始化、核心初始化、被驱动适配器初始化) 失败必须终止启动并报告原因(INV-I5)。 - 插件加载失败不得终止启动,记录警告并继续,触发优雅降级。 - 每步有超时保护,关键步骤超时抛 ``TimeoutError`` 终止启动。 - 异常处理保留原始错误上下文(INV-7),使用 ``raise ... from e``。 注:Outbox 恢复扫描(FR-22)、配对过期扫描(FR-33)与审计日志保留期 清理(FR-34)已迁移至 scheduler worker handler,不在本类启动。 """ #: Schema 初始化超时(秒) _SCHEMA_INIT_TIMEOUT: float = 60.0 #: 被驱动适配器初始化超时(秒) _DRIVEN_ADAPTER_INIT_TIMEOUT: float = 30.0 #: 配置加载超时(秒,INF-009) _CONFIG_LOAD_TIMEOUT: float = 30.0 #: 插件发现超时(秒) _PLUGIN_DISCOVER_TIMEOUT: float = 60.0 #: 单个插件加载超时(秒) # 注:必须大于内部 onInit 60s + onStart 60s = 120s 之和,否则外部超时 # 会先于内部 hook 超时触发,取消 ``_handle_failure`` 导致插件状态 # 不一致(P1-5) _PLUGIN_LOAD_TIMEOUT: float = 180.0 #: 驱动适配器注册超时(秒) _DRIVING_ADAPTER_REGISTER_TIMEOUT: float = 30.0 #: 路由绑定规则加载超时(秒):启动时从 DB 拉取启用规则到注册表 _ROUTE_BINDING_LOAD_TIMEOUT: float = 30.0 #: 后台任务启动探测窗口(秒):create_task 后短暂等待以捕获启动即失败 _TASK_STARTUP_PROBE_TIMEOUT: float = 0.5 def __init__( self, ensure_schema: _EnsureSchemaPort, di_container: DependencyInjectionContainer, plugin_dir: str, event_bus: EventBus, stage_slot_registry: StageSlotRegistry, event_subscription_registry: EventSubscriptionRegistry, config_source_registry: ConfigSourceRegistry, plugin_lifecycle_manager: PluginLifecycleManager, plugin_dependency_resolver: PluginDependencyResolver, stage_slot_injector: StageSlotInjector, health_aggregator: HealthAggregator, transport_manager: TransportManager, logger: LoggerPort, config_port: ConfigPort, driving_adapter_registrar: Callable[[], Awaitable[None]] | None = None, driving_adapter_unregistrar: Callable[[], Awaitable[None]] | None = None, app_driven_adapters: Sequence[Any] | None = None, ) -> None: """初始化宿主启动编排器。 Args: ensure_schema: 渠道 schema 初始化端口(``PostgresManager`` 满足 此 Protocol,提供 ``ensure_channel_schema`` 方法)。 di_container: 依赖注入容器,用于注册应用级单例。 plugin_dir: 插件目录路径,供 ``discover`` 扫描。 event_bus: 事件总线,注册到 DI 容器供领域事件分发。 stage_slot_registry: 阶段槽位注册中心。 event_subscription_registry: 事件订阅注册中心。 config_source_registry: 配置源注册中心。 plugin_lifecycle_manager: 插件生命周期管理器,编排 discover/load。 注册到 DI 容器供 ``PluginLifecycleService`` 委托调用。 plugin_dependency_resolver: 插件依赖解析器,Kahn 拓扑排序。 stage_slot_injector: 管道阶段插槽注入器。 health_aggregator: 健康状态聚合器,注册到 DI 容器供 ``HealthCheckService`` 委托调用。 logger: 日志端口,记录启动过程。 config_port: 配置被驱动端口(INF-009),启动期 ``_loadConfig`` 调用 ``getSchema`` 验证配置端口可用与 schema 已加载,配置 缺失或不可用必须终止启动(INV-I5 / §13.1)。 driving_adapter_registrar: 驱动适配器注册回调(可选),由调用方 提供,在步骤 7 调用以开始接受流量。为 ``None`` 时步骤 7 记录 告警并跳过(驱动适配器尚未接线)。 driving_adapter_unregistrar: 驱动适配器注销回调(可选),与 ``driving_adapter_registrar`` 对称,由调用方提供。当启动失败 回滚且 ``_driving_registered`` 为 ``True`` 时调用,以注销已 注册的驱动适配器并停止接收新流量。为 ``None`` 时回滚跳过 驱动适配器注销步骤(仅当调用方未提供注销能力时使用)。 app_driven_adapters: 应用级被驱动适配器列表(可选,INF-010)。 包含 ``RedisCacheAdapter`` / ``ChannelPersistenceAdapter`` / ``ARQQueueAdapter`` / ``RedisConfigAdapter`` 等跨请求共享的 应用级适配器实例。``_initDrivenAdapters`` 步骤对每个适配器 执行连通性检查(``ping()`` 或等效),失败抛 ``DependencyError`` 终止启动。为 ``None`` 或空时跳过连通性检查(向后兼容)。 """ self._ensure_schema = ensure_schema self._di = di_container self._plugin_dir = plugin_dir self._event_bus = event_bus self._stage_slot_registry = stage_slot_registry self._event_subscription_registry = event_subscription_registry self._config_source_registry = config_source_registry self._plugin_lifecycle = plugin_lifecycle_manager self._resolver = plugin_dependency_resolver self._stage_slot_injector = stage_slot_injector self._health_aggregator = health_aggregator self._transport_manager = transport_manager self._logger = logger self._config_port = config_port self._driving_adapter_registrar = driving_adapter_registrar self._driving_adapter_unregistrar = driving_adapter_unregistrar self._app_driven_adapters: tuple[Any, ...] = tuple(app_driven_adapters) if app_driven_adapters else () self._ready = False # 启动进行中标志:防止 ``bootstrap`` 并发或重复调用导致状态混乱 # (如后台任务引用被覆盖产生孤儿任务、DI 容器重复注册等)。 self._bootstrapping = False self._plugin_reload_task: asyncio.Task[None] | None = None self._transport_manager_task: asyncio.Task[None] | None = None # 路由绑定加载器:``_loadRouteBindingsFromDB`` 中构造,供 # ``_RouteBindingChangedHandler`` 持有以处理增量刷新事件。 self._route_binding_loader: RouteMatchRegistryLoader | None = None # 启动步骤完成跟踪标志(INF-013):用于 ``_rollback`` 判断哪些步骤 # 已成功执行需逆序回滚。仅在对应步骤成功路径末尾置 ``True``,失败 # 路径保持 ``False`` 以跳过该步回滚。 self._driving_registered: bool = False self._plugins_loaded: bool = False @property def isReady(self) -> bool: """宿主是否已就绪。""" return self._ready @property def pluginReloadTask(self) -> asyncio.Task[None] | None: """失败插件自动重载后台任务(供 ``HostShutdown`` 取消)。""" return self._plugin_reload_task @property def transportManagerTask(self) -> asyncio.Task[None] | None: """传输引擎管理器后台任务(供 ``HostShutdown`` 取消)。""" return self._transport_manager_task async def bootstrap(self) -> None: """执行启动编排。 关键步骤(Schema 初始化、核心初始化、被驱动适配器初始化) 失败必须抛异常导致宿主启动失败(INV-I5)。插件加载失败记录 警告并继续,触发优雅降级。每步有超时保护,关键步骤超时抛 ``TimeoutError`` 终止启动。 启动序列(INF-012:步骤 7 与步骤 8 紧邻,对齐规范 §15.1, 后台任务启动置于 ``_markReady`` 之后):: 0. _loadConfig 加载渠道配置(INF-009) 1. _initSchema 初始化渠道 schema 2. _initCore 注册中心 + EventBus + 领域服务 3. _initDrivenAdapters 被驱动适配器(含连通性检查 INF-010) 3.5 _loadRouteBindingsFromDB 加载路由绑定规则到注册表(DB → 内存) 4. _loadPlugins 渠道插件(按依赖拓扑) 5. _assemblePipelines 装配管道(含锚点校验 INF-011) 6. _registerDrivingAdapters 注册驱动适配器(开始接受流量) 7. _markReady 标记宿主为 ready 8. _startPluginReload 失败插件自动重载 Raises: Exception: 关键步骤失败时向上抛出,导致宿主启动失败(INV-I5)。 TimeoutError: 任一关键步骤超时。 InternalError: ``bootstrap`` 重复或并发调用。 """ # 入口守卫:防止重复/并发调用导致状态混乱(孤儿后台任务、DI 重复 # 注册、驱动适配器重复接线等)。启动失败后允许调用方重试,故仅在 # 进行中或已就绪时拒绝。 if self._ready: raise InternalError(message="host already bootstrapped") if self._bootstrapping: raise InternalError(message="bootstrap already in progress") self._bootstrapping = True try: await self._logger.info("宿主启动开始") # 0. 加载渠道配置(INF-009) # §13.1:配置缺失或类型不符必须在启动期失败,不得静默使用默认值。 await self._loadConfig() # 1. ensure_channel_schema() 初始化渠道 schema # INV-I5 / FF-SCHEMA-02:失败必须抛异常导致宿主启动失败 await self._initSchema() # 2. 初始化注册中心 + EventBus + 领域服务(注册到 DI 容器) await self._initCore() # 3. 加载并初始化被驱动适配器(INF-010:应用级适配器连通性检查) await self._initDrivenAdapters() # 3.5 加载路由绑定规则到 RouteMatchRegistry(DB → 内存注册表) # 并注册 RouteBindingChanged 事件订阅,供后续增量刷新 await self._loadRouteBindingsFromDB() # 4. 加载并初始化渠道插件(按依赖拓扑) # 插件失败标记 failed,触发优雅降级,不阻塞其他插件 await self._loadPlugins() # 4.5 注册通用命令(依赖已加载插件的 channel_type 列表) await self._registerCommonCommands() # 5. 装配管道(注入插件阶段) await self._assemblePipelines() # 6. 注册驱动适配器(开始接受流量) await self._registerDrivingAdapters() # 7. 标记宿主为 ready(INF-012:与步骤 6 紧邻,对齐规范 §15.1 # 要求"注册驱动适配器 → 标记 ready"不可被后台任务启动插入) await self._markReady() # 8. 启动失败插件自动重载(FR-36) await self._startPluginReload() # 9. 启动传输引擎管理器 await self._startTransportManager() await self._logger.info("宿主启动完成") except Exception as e: # 关键步骤失败(INV-I5 / INF-013):按 §15.2 逆序回滚已启动的 # 组件(注销驱动适配器 → 停止插件 → 取消后台任务),避免资源 # 泄漏与孤儿状态。随后重新抛出原始异常,保留错误上下文(INV-7), # 不掩盖根因。 await self._logger.error(f"宿主启动失败,执行回滚: {e}") await self._rollback() raise finally: # 无论成功或失败,重置启动进行中标志。成功后 ``_ready=True``, # 后续调用将被入口守卫拒绝;失败后允许调用方重试。 self._bootstrapping = False def _cancelBackgroundTasks(self) -> None: """取消已启动的后台任务(bootstrap 失败时回滚)。 使用 ``getattr`` 安全访问可能尚未创建的任务属性(失败可能发生在 任一后台任务启动之前),仅对未完成的任务触发取消。实际的任务等待 与异常记录由 ``HostShutdown._releaseCoreResourcesInternal`` 在关停 流程中完成,此处仅触发取消(``Task.cancel`` 为同步调度)以尽快 释放资源。 """ for task_attr in ( "pluginReloadTask", "transportManagerTask", ): task = getattr(self, task_attr, None) if task is not None and not task.done(): task.cancel() async def _startBackgroundTask( self, coro: Coroutine[Any, Any, None], name: str, log_message: str, ) -> asyncio.Task[None]: """启动后台任务并探测启动即失败(P0-2)。 创建后台任务后,通过 ``asyncio.wait`` 在短探测窗口内观察任务是否 立即失败(未捕获异常在首次 ``await`` 前抛出)。正常运行的任务 (进入循环体)不触发探测失败,返回 Task 对象供调用方持有。 任务注册 ``done_callback`` 以捕获探测窗口之外的意外结束(如 后台循环任务因未预期 bug 退出),避免任务静默死亡无人感知。 取消触发的结束不在 callback 中记录(属正常关停)。 Args: coro: 后台任务协程(如 ``transport_manager.run()``)。 name: 任务名称,用于日志与 ``DependencyError`` 标识。 log_message: 任务启动成功后的日志消息。 Returns: 已启动的 ``asyncio.Task`` 对象。 Raises: DependencyError: 任务在探测窗口内完成且携带异常(启动即失败)。 """ task = asyncio.create_task(coro) task.add_done_callback(self._makeBackgroundTaskDoneCallback(name)) done, _ = await asyncio.wait({task}, timeout=self._TASK_STARTUP_PROBE_TIMEOUT) if task in done and task.exception() is not None: exc = task.exception() await self._logger.error( f"后台任务启动即失败: task={name}, error={exc}", task=name, error_type=type(exc).__name__, error=str(exc), ) raise DependencyError(name, exc) from exc await self._logger.info(log_message) return task def _makeBackgroundTaskDoneCallback( self, name: str, ) -> Callable[[asyncio.Task[None]], None]: """构造后台任务结束回调。 返回的回调在任务结束时被调用: - 任务被取消(``cancelled()``):静默返回,属正常关停流程。 - 任务正常完成(无异常):静默返回。 - 任务携带异常:调度异步 ERROR 日志记录,避免后台循环任务内部 未预期 bug 导致任务静默死亡无人感知。 日志通过 ``asyncio.create_task`` 调度(回调为同步函数,无法直接 ``await``)。事件循环已关闭时忽略日志(关停期边缘场景)。 Args: name: 任务名称,用于日志标识。 Returns: 可注册给 ``Task.add_done_callback`` 的同步回调函数。 """ def _on_done(task: asyncio.Task[None]) -> None: if task.cancelled(): return exc = task.exception() if exc is None: return try: asyncio.create_task( self._logger.error( f"后台任务意外结束: task={name}, error={exc}", task=name, error_type=type(exc).__name__, error=str(exc), ) ) except RuntimeError: # 事件循环已关闭(关停期),无法调度日志,忽略 pass return _on_done async def _rollback(self) -> None: """启动失败时按 §15.2 逆序回滚已启动的组件(INF-013)。 回滚顺序与启动顺序相反,确保已启动的组件被正确清理: 1. **注销驱动适配器**:若 ``_driving_registered`` 为 ``True`` 且 ``driving_adapter_unregistrar`` 回调非 ``None``,调用之以停止 接收新流量。回调为 ``None`` 时跳过(调用方未提供注销能力)。 2. **停止已加载插件**:若 ``_plugins_loaded`` 为 ``True``,调用 ``PluginLifecycleManager.stopAllStartedPlugins`` 释放所有 ``STARTED`` / ``PAUSED`` 状态插件资源(封装内部迭代逻辑, 避免访问 ``_plugin_registry`` 私有属性)。 3. **取消后台任务**:复用 ``_cancelBackgroundTasks`` 取消 Outbox 扫描等已启动的后台任务。 单步回滚异常记录告警并继续,确保后续步骤仍能执行(INV-9 回退路径 完整性)。触发回滚的原始异常已由调用方 ``bootstrap`` 记录,此处 不重复记录,仅清理资源。 """ # 1. 注销驱动适配器(停止接收新流量) if self._driving_registered and self._driving_adapter_unregistrar is not None: try: await self._driving_adapter_unregistrar() except Exception as exc: await self._logger.error( "rollback: driving_adapter_unregistrar failed", error_type=type(exc).__name__, error=str(exc), ) # 2. 停止已加载的 STARTED/PAUSED 状态插件 # 通过 ``PluginLifecycleManager.stopAllStartedPlugins`` public API # 释放插件资源,避免访问其内部 ``_plugin_registry`` 私有属性 # (封装原则)。单插件停止异常已由 ``stopAllStartedPlugins`` # 内部记录告警并继续,此处仅需捕获整体异常以确保后续回滚步骤执行。 if self._plugins_loaded: try: await self._plugin_lifecycle.stopAllStartedPlugins() except Exception as exc: await self._logger.error( "rollback: stop plugins failed", error_type=type(exc).__name__, error=str(exc), ) # 3. 取消已启动的后台任务(``_cancelBackgroundTasks`` 为同步方法, # ``Task.cancel`` 仅调度取消不等待完成) try: self._cancelBackgroundTasks() except Exception as exc: await self._logger.error( "rollback: cancel background tasks failed", error_type=type(exc).__name__, error=str(exc), ) async def _initSchema(self) -> None: """初始化渠道 schema。 调用 ``ensure_channel_schema()`` 创建渠道网关表与索引。失败必须抛异常 导致宿主启动失败(INV-I5 / FF-SCHEMA-02)。 Raises: Exception: ``ensure_channel_schema`` 失败时向上抛出。 TimeoutError: schema 初始化超时。 """ try: await asyncio.wait_for( self._ensure_schema.ensure_channel_schema(), timeout=self._SCHEMA_INIT_TIMEOUT, ) except TimeoutError as e: await self._logger.error( f"渠道 schema 初始化超时: timeout={self._SCHEMA_INIT_TIMEOUT}s", ) raise OperationTimeoutError( timeout_ms=int(self._SCHEMA_INIT_TIMEOUT * 1000), message=f"渠道 schema 初始化超时: timeout={self._SCHEMA_INIT_TIMEOUT}s", ) from e except Exception as e: await self._logger.error(f"渠道 schema 初始化失败: {e}") raise # 显式失败,不掩盖根因(INV-I5) async def _loadConfig(self) -> None: """加载渠道配置(INF-009)。 调用 ``ConfigPort.getSchema`` 验证配置端口可用与 schema 已加载。 §13.1 要求"配置缺失或类型不匹配必须在启动期失败,不得静默使用 默认值"。本步骤在 schema 初始化前执行,确保后续 schema 初始化与 管道装配可基于已知有效的配置进行。 失败语义: - ``getSchema`` 返回空元组:视为配置未初始化,抛 ``ConfigValidationError`` 终止启动。 - ``getSchema`` 抛 ``DependencyError``(如 Redis 不可达): 向上抛出原始异常(INV-I5),由 ``bootstrap`` 的统一异常处理 路径记录并触发回滚。 - ``getSchema`` 抛其他异常:包装为 ``ConfigValidationError`` 并通过 ``raise ... from e`` 保留根因(INV-7)。 - 超时:抛 ``OperationTimeoutError`` 终止启动。 Raises: ConfigValidationError: 配置 schema 为空或加载失败。 OperationTimeoutError: 配置加载超时。 """ try: schema = await asyncio.wait_for( self._config_port.getSchema(), timeout=self._CONFIG_LOAD_TIMEOUT, ) except TimeoutError as e: await self._logger.error( f"渠道配置加载超时: timeout={self._CONFIG_LOAD_TIMEOUT}s", ) raise OperationTimeoutError( timeout_ms=int(self._CONFIG_LOAD_TIMEOUT * 1000), message=f"渠道配置加载超时: timeout={self._CONFIG_LOAD_TIMEOUT}s", ) from e except DependencyError: # 依赖故障(如 Redis 不可达)已含 dep 信息,向上抛出保留根因 await self._logger.error("渠道配置加载失败:依赖故障") raise except Exception as e: await self._logger.error(f"渠道配置加载失败: {e}") raise ConfigValidationError( [f"load config schema failed: {e}"], ) from e if not schema: await self._logger.error("渠道配置 schema 为空,配置未初始化") raise ConfigValidationError( ["config schema is empty, config not initialized"], ) await self._logger.info( "渠道配置已加载", schema_field_count=len(schema), ) async def _initCore(self) -> None: """初始化注册中心 + EventBus + 领域服务。 将 framework 层应用级单例(EventBus、4 个扩展点注册中心)注册到 DI 容器,供请求级 ``create_channel_use_cases`` 解析。核心注册为同步操作 (仅注册单例到 DI 容器,无 I/O),无超时保护需求。 """ await self._registerCoreSingletons() await self._logger.info("核心组件已注册到 DI 容器") async def _registerCoreSingletons(self) -> None: """将核心组件注册到 DI 容器。 注册两类应用级单例: - framework 扩展点:EventBus、StageSlotRegistry、 EventSubscriptionRegistry、ConfigSourceRegistry - framework 编排组件:PluginLifecycleManager、HealthAggregator (供 ``create_channel_use_cases`` resolve 并注入用例服务) 同时将 ``EventPublisherPort`` 绑定到同一 ``EventBus`` 实例,使应用层 通过端口契约获取事件发布能力,满足依赖方向铁律(INV-1)与端口实现 命名空间分离(§6.3)。 核心注册表(PluginRegistry、CapabilityRegistry 等)由调用方在 构造 DI 容器时注册,此处不重复注册。 """ self._di.registerSingleton(EventBus, self._event_bus) # EventPublisherPort 绑定到 EventBus 实例(EventBus 结构化满足 # EventPublisherPort Protocol,无需显式继承),使应用层通过端口 # 契约依赖,不直接 import framework 层 EventBus。 self._di.registerSingleton(EventPublisherPort, self._event_bus) self._di.registerSingleton(StageSlotRegistry, self._stage_slot_registry) self._di.registerSingleton(EventSubscriptionRegistry, self._event_subscription_registry) self._di.registerSingleton(ConfigSourceRegistry, self._config_source_registry) self._di.registerSingleton(PluginLifecycleManager, self._plugin_lifecycle) self._di.registerSingleton(HealthAggregator, self._health_aggregator) self._di.registerSingleton(TransportManager, self._transport_manager) async def _initDrivenAdapters(self) -> None: """加载并初始化被驱动适配器(INF-010:应用级适配器连通性检查)。 被驱动适配器为请求级组件,由 ``create_driven_adapters(db)`` 按请求创建, 共享同一 ``db`` session。本步骤对 ``HostBootstrap`` 构造时传入的应用级 被驱动适配器(``cache_port`` / ``persistence_port`` / ``queue_port`` / ``config_port`` 等跨请求共享实例)执行连通性检查,确保下游依赖可达: 任一失败必须抛 ``DependencyError`` 终止启动(INV-I5),避免启动后请求期 才发现依赖故障。 连通性检查策略(``_pingAdapter``): - 通过 ``PingablePort`` 显式契约判断适配器是否具备连通性探测能力 (INV-3 契约显式化)。 - 满足契约的适配器(``RedisCacheAdapter`` / ``ChannelPersistenceAdapter`` / ``RedisConfigAdapter`` / ``ARQQueueAdapter``)调用其 ``ping()`` 方法。 - 不满足契约的适配器(如 ``AgentRunAdapter``)显式跳过。 Raises: DependencyError: 任一应用级适配器连通性检查失败。 OperationTimeoutError: 被驱动适配器初始化超时。 """ try: await asyncio.wait_for( self._checkDrivenAdaptersConnectivity(), timeout=self._DRIVEN_ADAPTER_INIT_TIMEOUT, ) except TimeoutError as e: raise OperationTimeoutError( timeout_ms=int(self._DRIVEN_ADAPTER_INIT_TIMEOUT * 1000), message=f"被驱动适配器初始化超时: timeout={self._DRIVEN_ADAPTER_INIT_TIMEOUT}s", ) from e await self._logger.info( "应用级被驱动适配器连通性检查通过", adapter_count=len(self._app_driven_adapters), ) async def _checkDrivenAdaptersConnectivity(self) -> None: """对应用级被驱动适配器执行连通性检查(INF-010)。 遍历 ``_app_driven_adapters``,逐个调用 ``_pingAdapter``。任一适配器 连通性失败抛 ``DependencyError`` 终止启动。``_app_driven_adapters`` 为 空时仅记录请求级 factory 模式说明并返回(向后兼容未传入场景)。 """ if not self._app_driven_adapters: await self._logger.debug( "DI 容器就绪,被驱动适配器工厂可用(请求级,由 create_driven_adapters 按请求创建)", ) return for adapter in self._app_driven_adapters: await self._pingAdapter(adapter) async def _pingAdapter(self, adapter: Any) -> None: """对单个应用级被驱动适配器执行连通性检查。 通过 ``PingablePort`` 显式契约判断适配器是否具备连通性探测能力。 满足契约的适配器(``RedisCacheAdapter`` / ``ChannelPersistenceAdapter`` / ``RedisConfigAdapter`` / ``ARQQueueAdapter``)调用其 ``ping()`` 方法;不满足契约的适配器(如 ``AgentRunAdapter``)显式跳过。 ``ping()`` 返回 ``False`` 或抛异常时,抛 ``DependencyError`` 附上 适配器类型名,由 ``bootstrap`` 的统一异常处理路径记录并触发回滚。 Raises: DependencyError: 适配器 ping 返回 False 或抛异常。 """ adapter_name = type(adapter).__name__ if not isinstance(adapter, PingablePort): await self._logger.debug( f"被驱动适配器未实现 PingablePort 契约,跳过连通性检查: adapter={adapter_name}", ) return try: ok = await adapter.ping() except Exception as exc: await self._logger.error( f"被驱动适配器连通性检查异常: adapter={adapter_name}, error={exc}", adapter=adapter_name, error_type=type(exc).__name__, error=str(exc), ) raise DependencyError(adapter_name, exc) from exc if ok is False: await self._logger.error( f"被驱动适配器连通性检查失败: adapter={adapter_name}", adapter=adapter_name, ) raise DependencyError( adapter_name, Error(f"ping returned False: {adapter_name}"), ) async def _loadRouteBindingsFromDB(self) -> None: """启动时从 DB 加载路由绑定规则到 ``RouteMatchRegistry``。 从 DI 容器解析 ``RouteMatchRegistry`` 与 ``PersistencePort``(实现 ``RouteBindingRepositoryPort``),构造 ``RouteMatchRegistryLoader`` 调用 ``loadAll`` 全量加载启用状态的规则到注册表。加载完成后注册 ``RouteBindingChanged`` 事件订阅,后续路由绑定变更通过事件增量刷新 注册表。 失败语义: - DB 查询失败等异常向上抛出,终止启动(INV-I5:路由绑定规则为 ``RouteResolver`` 运行时依赖,加载失败必须显式失败)。 - 超时抛 ``OperationTimeoutError`` 终止启动。 - 单条非法规则由 loader 内部 try/except 记录 warn 跳过,不阻断 整体加载。 Raises: Exception: ``loadAll`` 失败时向上抛出(DB 故障等)。 OperationTimeoutError: 路由绑定规则加载超时。 """ registry = self._di.resolve(RouteMatchRegistry) persistence_port = self._di.resolve(PersistencePort) self._route_binding_loader = RouteMatchRegistryLoader( repo_port=persistence_port, registry=registry, logger=self._logger, ) try: loaded = await asyncio.wait_for( self._route_binding_loader.loadAll(), timeout=self._ROUTE_BINDING_LOAD_TIMEOUT, ) except TimeoutError as e: raise OperationTimeoutError( timeout_ms=int(self._ROUTE_BINDING_LOAD_TIMEOUT * 1000), message=f"路由绑定规则加载超时: timeout={self._ROUTE_BINDING_LOAD_TIMEOUT}s", ) from e await self._logger.info( "路由绑定规则已加载到注册表", loaded=loaded, ) # 注册 RouteBindingChanged 事件订阅:后续路由绑定变更通过事件 # 增量刷新注册表,保持内存注册表与 DB 一致。 self._event_bus.register( "host", EventSubscription( event_type=ROUTE_BINDING_CHANGED, handler=_RouteBindingChangedHandler(self._route_binding_loader), priority=100, failure_policy=FailurePolicy.DEGRADE, ), ) async def _loadPlugins(self) -> None: """加载并初始化渠道插件(按依赖拓扑)。 执行 ``discover`` 扫描插件目录 → ``resolve`` 拓扑排序 → 按序 ``load``。 discover/resolve 失败记录警告并跳过插件加载;单个插件 load 失败记录 警告并继续,触发优雅降级,不阻塞宿主启动。 F-01:discover 成功后,从 DI 容器解析 ``SensitiveFieldRegistry``, 调用 ``register(manifests)`` 合并插件 manifest 声明的敏感字段, 并通过 ``configureSensitiveFieldRegistry`` 注入到 ``core/event/config.py`` 模块级全局变量,供 ``ConfigChanged`` / ``ConfigRollback`` 事件序列化脱敏使用。 F-02:discover 成功后,从 DI 容器解析 ``ConfigScopeRegistry``, 调用 ``register(manifests)`` 合并插件 manifest 声明的作用域, 供 ``RedisConfigAdapter._build_key`` 校验 key→scope 一致性。 Raises: TimeoutError: discover 阶段超时。 """ # discover 扫描插件目录 try: manifests = await asyncio.wait_for( self._plugin_lifecycle.discover(self._plugin_dir), timeout=self._PLUGIN_DISCOVER_TIMEOUT, ) except TimeoutError as e: raise OperationTimeoutError( timeout_ms=int(self._PLUGIN_DISCOVER_TIMEOUT * 1000), message=f"插件发现超时: timeout={self._PLUGIN_DISCOVER_TIMEOUT}s", ) from e except Exception as e: await self._logger.warn(f"插件发现失败,跳过插件加载: {e}") return if not manifests: await self._logger.info("无插件可加载") return await self._logger.info(f"发现 {len(manifests)} 个插件") # F-01:合并插件 manifest 声明的敏感字段到 SensitiveFieldRegistry, # 并注入到 core/event/config.py 模块级全局变量。registry 在 # _register_di_singletons 中已注册为单例(预合并 CONFIG_SCHEMA), # 此处增量合并插件声明的敏感字段。configureSensitiveFieldRegistry # 使 ConfigChanged/ConfigRollback 事件序列化时能识别插件声明的 # 敏感字段(如 bridge_url/bot_token/app_secret 等)。 sensitive_registry = self._di.resolve(SensitiveFieldRegistry) sensitive_registry.register(manifests) configureSensitiveFieldRegistry(sensitive_registry) # F-02:合并插件 manifest 声明的作用域到 ConfigScopeRegistry。 # registry 在 _register_di_singletons 中已注册为单例(预合并 # CONFIG_SCHEMA),此处增量合并插件声明的作用域。registry 通过 # dict.update 原地修改内部 dict,已持有 key_to_scope_map 只读视图的 # RedisConfigAdapter 实例立即可见更新。 config_scope_registry = self._di.resolve(ConfigScopeRegistry) config_scope_registry.register(manifests) # resolve 拓扑排序(失败则使用未排序顺序) try: sorted_manifests = self._resolver.resolve(manifests) except Exception as e: await self._logger.warn( f"插件依赖解析失败,使用未排序顺序加载: {e}", ) sorted_manifests = manifests # 按拓扑序加载 for manifest in sorted_manifests: await self._loadSinglePlugin(manifest) # INF-013:插件加载步骤完成(含优雅降级场景),置位跟踪标志。 # ``_rollback`` 据此判断是否需要停止 STARTED/PAUSED 状态插件; # 单个插件 load 失败已由 ``_loadSinglePlugin`` 记录告警并继续, # 不影响本步骤的完成状态。 self._plugins_loaded = True async def _registerCommonCommands(self) -> None: """注册通用命令到命令注册表。 从 DI 容器解析 ``PluginRegistry`` 与 ``CommandRegistryTable``, 遍历已加载插件的 channel_type 列表,为每个渠道类型注册 6 个通用 命令(/help、/model、/reset、/about、/allowlist、/approve)。 渠道特定命令优先保留,通用命令仅填充未占用的命令名。 本步骤必须在 ``_loadPlugins`` 之后执行,因为 channel_type 列表 由插件 manifest 声明,只有在插件加载后才可知。 """ plugin_registry = self._di.resolve(PluginRegistry) command_registry_table = self._di.resolve(CommandRegistryTable) channel_types = plugin_registry.listChannelTypes() registerCommonCommands(command_registry_table, channel_types) await self._logger.info( f"通用命令注册完成: {len(channel_types)} 个渠道类型", channel_count=len(channel_types), ) async def _loadSinglePlugin(self, manifest: ChannelManifest) -> None: """加载单个插件,失败记录警告并继续。 插件加载失败不阻塞宿主启动,记录警告并触发优雅降级。 Args: manifest: 渠道清单。 """ try: result: LifecycleResult = await asyncio.wait_for( self._plugin_lifecycle.load(manifest.id), timeout=self._PLUGIN_LOAD_TIMEOUT, ) except TimeoutError: await self._logger.warn( f"插件加载超时,跳过: plugin_id={manifest.id}, timeout={self._PLUGIN_LOAD_TIMEOUT}s", plugin_id=manifest.id, ) return except Exception as e: await self._logger.warn( f"插件加载异常,跳过: plugin_id={manifest.id}, error={e}", plugin_id=manifest.id, error=str(e), ) return if result.state == "failed": await self._logger.warn( f"插件加载失败,触发优雅降级: plugin_id={manifest.id}, error={result.error}", plugin_id=manifest.id, error=result.error or "", ) else: await self._logger.info( f"插件加载成功: plugin_id={manifest.id}", plugin_id=manifest.id, ) async def _assemblePipelines(self) -> None: """装配管道(注入插件阶段,INF-011:注入锚点格式校验)。 管道为请求级组件,由 ``create_channel_use_cases`` 按请求创建。阶段插槽 注入(``StageSlotInjector.inject``)在请求级管道装配时执行。本步骤在 启动期对注册中心所有声明式槽位执行一次注入验证(锚点格式合法、 ``replace`` 白名单约束、目标 ID 非空),提前发现非法锚点,避免请求期 ``inject`` 才抛 ``RuleViolationError`` 影响可用性。槽位校验为同步操作 (无 I/O),无超时保护需求。 验证失败必须抛 ``ValidationError`` 终止启动(INV-I5)。 Raises: ValidationError: 槽位锚点格式非法或 ``replace`` 用于非白名单阶段。 """ await self._validateAndLogPipelines() async def _validateAndLogPipelines(self) -> None: """校验所有声明式管道槽位锚点并记录就绪状态(INF-011)。 先调用 ``StageSlotInjector.validateAnchors`` 对所有管道的槽位锚点 做格式与白名单校验,失败抛 ``ValidationError``;通过后再记录各管道 槽位数量,供运维定位。 """ # 1. 启动期锚点注入验证(不依赖请求级 stages 列表) self._stage_slot_injector.validateAnchors() # 2. 记录已注册槽位数量 inbound_slots = self._stage_slot_registry.findByPipeline("inbound") outbound_slots = self._stage_slot_registry.findByPipeline("outbound") control_slots = self._stage_slot_registry.findByPipeline("control") await self._logger.info( "管道阶段槽位就绪(请求级注入,锚点校验通过)", inbound_slot_count=len(inbound_slots), outbound_slot_count=len(outbound_slots), control_slot_count=len(control_slots), ) async def _registerDrivingAdapters(self) -> None: """注册驱动适配器(开始接受流量)。 若调用方提供了 ``driving_adapter_registrar`` 回调,则调用之以注册 各驱动适配器(如 Webhook / Admin API / 内置 Web 渠道等,具体由 宿主接线决定)。为 ``None`` 时记录告警,宿主不开始接受流量 (驱动适配器尚未接线)。 Raises: TimeoutError: 驱动适配器注册超时。 """ if self._driving_adapter_registrar is None: await self._logger.warn( "驱动适配器注册回调未提供,宿主不开始接受流量(driving_adapter_registrar=None)", ) return try: await asyncio.wait_for( self._driving_adapter_registrar(), timeout=self._DRIVING_ADAPTER_REGISTER_TIMEOUT, ) except TimeoutError as e: raise OperationTimeoutError( timeout_ms=int(self._DRIVING_ADAPTER_REGISTER_TIMEOUT * 1000), message=f"驱动适配器注册超时: timeout={self._DRIVING_ADAPTER_REGISTER_TIMEOUT}s", ) from e await self._logger.info("驱动适配器已注册,开始接受流量") # INF-013:驱动适配器注册成功,置位跟踪标志。``_rollback`` 据此 # 判断是否需要调用 ``driving_adapter_unregistrar`` 注销驱动适配器。 self._driving_registered = True async def _startPluginReload(self) -> None: """启动失败插件自动重载后台任务(FR-36)。 以后台任务方式启动 ``PluginLifecycleManager.reloadLoop``,定时 (默认 60s 间隔)扫描 ``DegradationManager`` 中到达 ``next_retry_at`` 的失败插件并触发重载。``reloadLoop`` 内部已捕获并记录单轮扫描异常, 不阻塞宿主启动。通过 ``_startBackgroundTask`` 启动探测捕获启动即失败。 Raises: DependencyError: 后台任务启动即失败。 """ self._plugin_reload_task = await self._startBackgroundTask( self._plugin_lifecycle.reloadLoop(interval=60.0), name="plugin_reload", log_message="失败插件自动重载已启动(后台任务,间隔 60s)", ) async def _startTransportManager(self) -> None: """启动传输引擎管理器。 以后台任务方式启动 ``TransportManager.start()``,统一管理所有 渠道账户的入站传输 Worker(Puller/Stream)。通过 ``_startBackgroundTask`` 启动探测捕获启动即失败。 Raises: DependencyError: 后台任务启动即失败。 """ self._transport_manager_task = await self._startBackgroundTask( self._transport_manager.start(), name="transport_manager", log_message="传输引擎管理器已启动", ) async def _markReady(self) -> None: """标记宿主为 ready。 ``_setReadyState`` 为同步操作(仅置位标志与记录日志,无 I/O), 无超时保护需求。 """ await self._setReadyState() async def _setReadyState(self) -> None: """设置宿主就绪状态。""" self._ready = True await self._logger.info("宿主已标记为 ready")