ForcePilot/backend/package/yuxi/channels/infrastructure/host_bootstrap.py

1056 lines
50 KiB
Python
Raw Normal View History

"""宿主启动编排器。
本模块是编排层的启动编排入口按顺序装配渠道网关的所有
组件关键步骤失败必须终止启动并报告原因INV-I5
插件加载失败不得终止启动必须触发优雅降级
启动顺序不可颠倒
1. 加载渠道配置INF-009配置缺失或类型不符必须抛异常
2. ``ensure_channel_schema()`` 初始化渠道 schemaINV-I5失败必须抛异常
3. 初始化注册中心 + EventBus + 领域服务注册到 DI 容器
4. 加载并初始化被驱动适配器INF-010应用级适配器连通性检查
5. 加载并初始化渠道插件按依赖拓扑失败记录警告继续
6. 装配管道INF-011注入锚点格式校验
7. 注册驱动适配器开始接受流量
8. 标记宿主为 readyINF-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. 标记宿主为 readyINF-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 加载路由绑定规则到 RouteMatchRegistryDB → 内存注册表)
# 并注册 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. 标记宿主为 readyINF-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 层应用级单例EventBus4 个扩展点注册中心注册到 DI
容器供请求级 ``create_channel_use_cases`` 解析核心注册为同步操作
仅注册单例到 DI 容器 I/O无超时保护需求
"""
await self._registerCoreSingletons()
await self._logger.info("核心组件已注册到 DI 容器")
async def _registerCoreSingletons(self) -> None:
"""将核心组件注册到 DI 容器。
注册两类应用级单例
- framework 扩展点EventBusStageSlotRegistry
EventSubscriptionRegistryConfigSourceRegistry
- framework 编排组件PluginLifecycleManagerHealthAggregator
``create_channel_use_cases`` resolve 并注入用例服务
同时将 ``EventPublisherPort`` 绑定到同一 ``EventBus`` 实例使应用层
通过端口契约获取事件发布能力满足依赖方向铁律INV-1与端口实现
命名空间分离§6.3
核心注册表PluginRegistryCapabilityRegistry 由调用方在
构造 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-01discover 成功后 DI 容器解析 ``SensitiveFieldRegistry``
调用 ``register(manifests)`` 合并插件 manifest 声明的敏感字段
并通过 ``configureSensitiveFieldRegistry`` 注入到
``core/event/config.py`` 模块级全局变量 ``ConfigChanged`` /
``ConfigRollback`` 事件序列化脱敏使用
F-02discover 成功后 DI 容器解析 ``ConfigScopeRegistry``
调用 ``register(manifests)`` 合并插件 manifest 声明的作用域
``RedisConfigAdapter._build_key`` 校验 keyscope 一致性
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()``统一管理所有
渠道账户的入站传输 WorkerPuller/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")