本次提交是一次大型架构重构,核心变更包括: 1. 调整持久化适配器为无状态实现,通过session_factory按需获取会话 2. 重构事务共享与透传机制,统一使用_session_scope管理会话生命周期 3. 移除OutboxEntry聚合根内的版本自增逻辑,由持久化层统一管理 4. 优化微信插件会话类型枚举与配置对齐 5. 简化出站管道阶段依赖注入与流程逻辑 6. 删除健康检查自动释放会话的冗余代码 7. 重构多个定时任务处理器,移除显式会话工厂创建逻辑 8. 修复会话延迟加载异常问题,新增时区转换工具函数
1095 lines
52 KiB
Python
1095 lines
52 KiB
Python
"""宿主启动编排器。
|
||
|
||
本模块是编排层的启动编排入口,按顺序装配渠道网关的所有
|
||
组件。关键步骤失败必须终止启动并报告原因(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
|
||
# 路由绑定规则加载完成标志(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 加载路由绑定规则到 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. **清理路由绑定注册表**:若 ``_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-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")
|