ForcePilot/backend/package/yuxi/channels/infrastructure/host_bootstrap.py
Kris bb1934023e refactor: 完成会话持久化与事务机制重构,清理冗余代码
本次提交是一次大型架构重构,核心变更包括:
1. 调整持久化适配器为无状态实现,通过session_factory按需获取会话
2. 重构事务共享与透传机制,统一使用_session_scope管理会话生命周期
3. 移除OutboxEntry聚合根内的版本自增逻辑,由持久化层统一管理
4. 优化微信插件会话类型枚举与配置对齐
5. 简化出站管道阶段依赖注入与流程逻辑
6. 删除健康检查自动释放会话的冗余代码
7. 重构多个定时任务处理器,移除显式会话工厂创建逻辑
8. 修复会话延迟加载异常问题,新增时区转换工具函数
2026-07-10 04:10:33 +08:00

1095 lines
52 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.

"""宿主启动编排器。
本模块是编排层的启动编排入口,按顺序装配渠道网关的所有
组件。关键步骤失败必须终止启动并报告原因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
# 路由绑定规则加载完成标志H-17``_loadRouteBindingsFromDB``
# 成功完成后置位,``_rollback`` 据此判断是否需要清理
# ``RouteMatchRegistry`` 中已加载的绑定规则,避免部分加载失败
# 后留下孤儿规则。
self._route_bindings_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. **清理路由绑定注册表**:若 ``_route_bindings_loaded`` 为 ``True``
H-17调用 ``RouteMatchRegistry.clear`` 清空已加载的绑定规则,
避免部分加载失败(如超时)后留下孤儿规则。``clear`` 为同步方法
仅清空绑定索引,不清理 tier 与 matcher。
4. **取消后台任务**:复用 ``_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. 清理路由绑定注册表H-17若 ``_loadRouteBindingsFromDB`` 已
# 成功完成,调用 ``RouteMatchRegistry.clear`` 清空已加载的绑定
# 规则,避免部分加载失败(如超时)后留下孤儿规则。``clear`` 为
# 同步方法(``threading.Lock`` 保护),不清理 tier 与 matcher。
# 清理后立即重置标志以保证 ``_rollback`` 幂等(可重复调用不重复清理)。
if self._route_bindings_loaded:
try:
registry = self._di.resolve(RouteMatchRegistry)
registry.clear()
except Exception as exc:
await self._logger.error(
"rollback: clear route match registry failed",
error_type=type(exc).__name__,
error=str(exc),
)
self._route_bindings_loaded = False
# 4. 取消已启动的后台任务(``_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,
)
# 非阻塞创建 GIN 索引C-6失败不阻断启动
try:
await self._ensure_schema.ensureChannelIndexesConcurrently()
except Exception as idx_exc:
await self._logger.warning(
f"渠道 GIN 索引并发创建失败(不阻断启动,下次重试): {idx_exc}",
)
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, session_factory, ...)``
按请求创建,其中 ``conversation`` / ``transaction`` 共享同一 ``db`` session
``persistence`` 为无状态适配器(注入 ``session_factory``)。本步骤对
``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,
),
)
# H-17路由绑定规则加载完成置位跟踪标志。``_rollback`` 据此
# 判断是否需要清理 ``RouteMatchRegistry``,避免部分加载失败后
# 留下孤儿规则。
self._route_bindings_loaded = True
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`` 校验 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()``,统一管理所有
渠道账户的入站传输 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")