1. 移除所有适配器文件中多余的空导入行 2. 调整ValidationError继承,移除不必要的ValueError继承 3. 修正多处ChannelType使用方式,从.value改为直接使用枚举实例 4. 优化飞书插件部分硬编码渠道类型为枚举实例 5. 更新wechat_ilink插件清单与适配器配置 6. 新增飞书目录适配器缓存清理支持判断与iLink生命周期适配器凭据轮换支持判断 7. 优化配置处理器历史查询逻辑,区分键不存在与无历史记录场景
581 lines
26 KiB
Python
581 lines
26 KiB
Python
"""宿主关停编排器。
|
||
|
||
本模块是编排层的关停编排入口,按反向顺序释放渠道网关
|
||
资源。每步有超时,超时强制进入下一步并告警,
|
||
关停不得丢弃在途请求(除非超时强制终止)。
|
||
|
||
关停顺序:
|
||
1. 标记宿主为 draining,停止接受新请求
|
||
2. 等待在途请求完成(带超时 30s)
|
||
3. 拆除管道
|
||
4. 停止传输引擎管理器(入站Worker)
|
||
5. 停止渠道插件(按反向依赖拓扑)
|
||
6. 关闭被驱动适配器
|
||
7. 释放领域核心资源
|
||
|
||
注:§15.2 规范定义了"注销驱动适配器"步骤,当前系统未接入驱动适配器
|
||
机制(``driving_adapter_registrar`` / ``driving_adapter_unregistrar`` 均
|
||
未实现),故此处省略该步骤。当驱动适配器实现就绪后,需在步骤 2 与 3
|
||
之间补充注销逻辑,并恢复与规范一致的 7 步序列。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import asyncio
|
||
from collections.abc import Awaitable, Callable, Sequence
|
||
from typing import Any
|
||
|
||
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.lifecycle.plugin_dependency_resolver import (
|
||
PluginDependencyResolver,
|
||
)
|
||
from yuxi.channels.application.lifecycle.plugin_lifecycle_manager import (
|
||
PluginLifecycleManager,
|
||
)
|
||
from yuxi.channels.application.outbox.outbox_recovery_scanner import (
|
||
OutboxRecoveryScanner,
|
||
)
|
||
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.errors.server import OperationTimeoutError
|
||
from yuxi.channels.contract.plugin.lifecycle import LifecycleState
|
||
from yuxi.channels.contract.plugin.manifest import ChannelManifest
|
||
from yuxi.channels.contract.ports.driven.closeable_port import CloseablePort
|
||
from yuxi.channels.contract.ports.driven.logger_port import LoggerPort
|
||
from yuxi.channels.core.registry.plugin_registry import PluginRegistry
|
||
from yuxi.channels.infrastructure.host_bootstrap import HostBootstrap
|
||
|
||
__all__ = [
|
||
"HostShutdown",
|
||
]
|
||
|
||
|
||
class HostShutdown:
|
||
"""宿主关停编排器。
|
||
|
||
按反向顺序释放渠道网关资源。
|
||
|
||
关停顺序:
|
||
1. 标记宿主为 draining,停止接受新请求
|
||
2. 等待在途请求完成(带超时 30s)
|
||
3. 拆除管道
|
||
4. 停止传输引擎管理器(入站Worker)
|
||
5. 停止渠道插件(按反向依赖拓扑)
|
||
6. 关闭被驱动适配器
|
||
7. 释放领域核心资源
|
||
|
||
关键约束:
|
||
- 每个步骤 **必须** 有超时,超时后强制进入下一步并告警。
|
||
- 关停 **不得** 丢弃在途请求(除非超时强制终止)。
|
||
- 插件停止 **必须** 按反向依赖拓扑顺序,被依赖者后停止。
|
||
- 关停过程中任一步异常不得中断后续步骤,记录告警后继续。
|
||
|
||
注:§15.2 规范定义了"注销驱动适配器"步骤,当前系统未接入驱动适配器
|
||
机制,故省略该步骤。详见模块级文档字符串。
|
||
"""
|
||
|
||
#: 在途请求等待超时(秒),超时强制进入下一步并告警
|
||
SHUTDOWN_TIMEOUT: float = 30.0
|
||
|
||
#: 标记 draining 超时(秒)
|
||
_MARK_DRAINING_TIMEOUT: float = 10.0
|
||
#: 拆除管道超时(秒)
|
||
_TEARDOWN_PIPELINE_TIMEOUT: float = 30.0
|
||
#: 停止传输引擎管理器超时(秒)
|
||
_STOP_TRANSPORT_MANAGER_TIMEOUT: float = 30.0
|
||
#: 单个插件停止超时(秒)
|
||
_STOP_PLUGIN_TIMEOUT: float = 30.0
|
||
#: 关闭被驱动适配器超时(秒)
|
||
_CLOSE_DRIVEN_TIMEOUT: float = 30.0
|
||
#: 释放核心资源超时(秒)
|
||
_RELEASE_CORE_TIMEOUT: float = 30.0
|
||
|
||
def __init__(
|
||
self,
|
||
plugin_lifecycle_manager: PluginLifecycleManager,
|
||
plugin_registry: PluginRegistry,
|
||
plugin_dependency_resolver: PluginDependencyResolver,
|
||
stage_slot_injector: StageSlotInjector,
|
||
stage_slot_registry: StageSlotRegistry,
|
||
event_subscription_registry: EventSubscriptionRegistry,
|
||
config_source_registry: ConfigSourceRegistry,
|
||
event_bus: EventBus,
|
||
outbox_recovery_scanner: OutboxRecoveryScanner,
|
||
transport_manager: TransportManager,
|
||
logger: LoggerPort,
|
||
host_bootstrap: HostBootstrap,
|
||
inflight_drain_waiter: Callable[[float], Awaitable[None]] | None = None,
|
||
app_driven_adapters: Sequence[Any] | None = None,
|
||
) -> None:
|
||
"""初始化宿主关停编排器。
|
||
|
||
Args:
|
||
plugin_lifecycle_manager: 插件生命周期管理器,编排 stop/unload。
|
||
plugin_registry: 插件注册表,按状态列出待停止插件。
|
||
plugin_dependency_resolver: 插件依赖解析器,用于计算反向拓扑序。
|
||
stage_slot_injector: 管道阶段插槽注入器。
|
||
stage_slot_registry: 阶段槽位注册中心。
|
||
event_subscription_registry: 事件订阅注册中心。
|
||
config_source_registry: 配置源注册中心。
|
||
event_bus: 事件总线。
|
||
outbox_recovery_scanner: Outbox 恢复扫描器。
|
||
transport_manager: 传输引擎管理器,在关停步骤4调用stop()停止入站Worker。
|
||
logger: 日志端口,记录关停过程。
|
||
host_bootstrap: 宿主启动编排器实例。在步骤 7 释放核心资源时,
|
||
惰性读取其 ``outboxScanTask`` / ``pairingScanTask`` /
|
||
``auditLogRetentionScanTask`` / ``pluginReloadTask`` /
|
||
``transportManagerTask`` 属性
|
||
并取消未完成的后台任务。传入实例而非任务引用,是为了在
|
||
``HostShutdown`` 创建于 ``bootstrap()`` 之前时仍能取到
|
||
``bootstrap()`` 执行后填充的任务引用。
|
||
inflight_drain_waiter: 在途请求排空回调(可选),接受超时参数,
|
||
在步骤 2 调用以等待在途请求完成。为 ``None`` 时步骤 2 直接
|
||
等待固定超时。
|
||
app_driven_adapters: 应用级被驱动适配器列表(可选,INF-017)。
|
||
包含 ``RedisCacheAdapter`` / ``ChannelPersistenceAdapter`` /
|
||
``ARQQueueAdapter`` / ``RedisConfigAdapter`` 等在
|
||
``HostBootstrap`` 构造时创建、跨请求共享的应用级适配器实例。
|
||
在关停步骤 5 显式调用 ``close()`` 释放 Redis 连接、DB 会话等
|
||
资源。为 ``None`` 或空时跳过应用级适配器关闭(向后兼容)。
|
||
"""
|
||
self._plugin_lifecycle = plugin_lifecycle_manager
|
||
self._plugin_registry = plugin_registry
|
||
self._resolver = plugin_dependency_resolver
|
||
self._stage_slot_injector = stage_slot_injector
|
||
self._stage_slot_registry = stage_slot_registry
|
||
self._event_subscription_registry = event_subscription_registry
|
||
self._config_source_registry = config_source_registry
|
||
self._event_bus = event_bus
|
||
self._outbox_scanner = outbox_recovery_scanner
|
||
self._transport_manager = transport_manager
|
||
self._logger = logger
|
||
self._inflight_drain_waiter = inflight_drain_waiter
|
||
self._host_bootstrap = host_bootstrap
|
||
self._app_driven_adapters: tuple[Any, ...] = tuple(app_driven_adapters) if app_driven_adapters else ()
|
||
self._draining = False
|
||
|
||
async def shutdown(self) -> None:
|
||
"""执行关停编排。
|
||
|
||
每步有超时,超时强制进入下一步并告警。关停不得丢弃在途
|
||
请求(除非超时强制终止)。插件停止按反向依赖拓扑顺序,被依赖者
|
||
后停止。任一步异常不中断后续步骤,记录告警后继续。
|
||
"""
|
||
await self._logger.info("宿主关停开始")
|
||
|
||
# 1. 标记宿主为 draining,停止接受新请求
|
||
await self._markDraining()
|
||
|
||
# 2. 等待在途请求完成(带超时)
|
||
await self._waitForInflightRequests(self.SHUTDOWN_TIMEOUT)
|
||
|
||
# 3. 拆除管道
|
||
await self._teardownPipelines()
|
||
|
||
# 4. 停止传输引擎管理器(入站Worker)
|
||
await self._stopTransportManager()
|
||
|
||
# 5. 停止渠道插件(按反向依赖拓扑)
|
||
await self._stopPlugins()
|
||
|
||
# 6. 关闭被驱动适配器
|
||
await self._closeDrivenAdapters()
|
||
|
||
# 7. 释放领域核心资源
|
||
await self._releaseCoreResources()
|
||
|
||
await self._logger.info("宿主关停完成")
|
||
|
||
async def _markDraining(self) -> None:
|
||
"""标记宿主为 draining,停止接受新请求。
|
||
|
||
超时强制进入下一步并告警。
|
||
"""
|
||
try:
|
||
await asyncio.wait_for(
|
||
self._setDrainingState(),
|
||
timeout=self._MARK_DRAINING_TIMEOUT,
|
||
)
|
||
# 将内置 asyncio.TimeoutError 翻译为契约 OperationTimeoutError 记录结构化告警
|
||
# (硬约束:所有模块必须使用自定义异常类而非原生异常)
|
||
except TimeoutError:
|
||
await self._handleStepTimeout("标记 draining", self._MARK_DRAINING_TIMEOUT)
|
||
except Exception as e:
|
||
await self._logger.warn(
|
||
f"标记 draining 异常,强制进入下一步: {e}",
|
||
error_type=type(e).__name__,
|
||
error=str(e),
|
||
)
|
||
|
||
async def _setDrainingState(self) -> None:
|
||
"""设置 draining 状态。"""
|
||
self._draining = True
|
||
await self._logger.info("宿主已标记为 draining,停止接受新请求")
|
||
|
||
async def _handleStepTimeout(self, step_name: str, timeout_seconds: float) -> None:
|
||
"""记录关停步骤超时告警(将内置 ``TimeoutError`` 翻译为契约 ``OperationTimeoutError``)。
|
||
|
||
满足硬约束:所有模块必须使用自定义异常类而非原生异常
|
||
(e.g., ``TimeoutError``, ``ValueError``),防止原生异常穿透至核心层。
|
||
shutdown 路径属降级处理:将内置 ``asyncio.TimeoutError`` 翻译为
|
||
``OperationTimeoutError`` 后仅记录结构化告警,不重新抛出,避免中断
|
||
后续关停步骤(与 ``HostBootstrap`` 关键步骤"包装并终止"的语义不同,
|
||
shutdown 要求"超时强制进入下一步")。
|
||
|
||
Args:
|
||
step_name: 步骤名称,用于告警标识。
|
||
timeout_seconds: 超时秒数,转换为 ``timeout_ms`` 写入告警字段。
|
||
"""
|
||
timeout_err = OperationTimeoutError(
|
||
timeout_ms=int(timeout_seconds * 1000),
|
||
message=f"{step_name} 超时: timeout={timeout_seconds}s",
|
||
)
|
||
await self._logger.warn(
|
||
f"{step_name} 超时,强制进入下一步",
|
||
step=step_name,
|
||
timeout_ms=timeout_err.timeout_ms,
|
||
error_type=type(timeout_err).__name__,
|
||
error=timeout_err.message,
|
||
)
|
||
|
||
async def _waitForInflightRequests(self, timeout: float) -> None:
|
||
"""等待在途请求完成(带超时)。
|
||
|
||
若调用方提供了 ``inflight_drain_waiter`` 回调,则调用之并传入超时
|
||
参数,由回调内部实现超时(如 ``InflightRequestTracker.wait_drained``
|
||
使用 ``asyncio.wait_for`` 等待归零事件)。为 ``None`` 时等待固定超时
|
||
后继续。超时强制进入下一步并告警。
|
||
|
||
Args:
|
||
timeout: 等待超时时间(秒),超时强制进入下一步并告警。
|
||
"""
|
||
try:
|
||
if self._inflight_drain_waiter is not None:
|
||
await self._inflight_drain_waiter(timeout)
|
||
else:
|
||
await self._logger.info(
|
||
f"无在途请求排空回调,直接继续: timeout={timeout}s",
|
||
)
|
||
# 将内置 asyncio.TimeoutError 翻译为契约 OperationTimeoutError 记录结构化告警
|
||
# (硬约束:所有模块必须使用自定义异常类而非原生异常)
|
||
except TimeoutError:
|
||
await self._handleStepTimeout("等待在途请求", timeout)
|
||
except Exception as e:
|
||
await self._logger.warn(
|
||
f"等待在途请求异常,强制进入下一步: {e}",
|
||
error_type=type(e).__name__,
|
||
error=str(e),
|
||
)
|
||
|
||
async def _teardownPipelines(self) -> None:
|
||
"""拆除管道(INF-016:清理阶段槽位注册)。
|
||
|
||
管道为请求级组件,在途请求完成后已无活跃管道实例。本步骤记录管道
|
||
拆除状态,并调用 ``StageSlotRegistry.clear`` 清理所有插件注册的
|
||
阶段槽位,确保下一次 ``bootstrap()`` 不会复用上次启动残留的槽位
|
||
(否则 ``_assemblePipelines`` 的锚点校验可能命中陈旧锚点)。超时
|
||
强制进入下一步并告警。
|
||
"""
|
||
try:
|
||
await asyncio.wait_for(
|
||
self._teardownPipelinesInternal(),
|
||
timeout=self._TEARDOWN_PIPELINE_TIMEOUT,
|
||
)
|
||
# 将内置 asyncio.TimeoutError 翻译为契约 OperationTimeoutError 记录结构化告警
|
||
# (硬约束:所有模块必须使用自定义异常类而非原生异常)
|
||
except TimeoutError:
|
||
await self._handleStepTimeout("拆除管道", self._TEARDOWN_PIPELINE_TIMEOUT)
|
||
except Exception as e:
|
||
await self._logger.warn(
|
||
f"拆除管道异常,强制进入下一步: {e}",
|
||
error_type=type(e).__name__,
|
||
error=str(e),
|
||
)
|
||
|
||
async def _teardownPipelinesInternal(self) -> None:
|
||
"""记录管道拆除状态并清理阶段槽位注册(INF-016)。"""
|
||
inbound_slots = self._stage_slot_registry.findByPipeline("inbound")
|
||
outbound_slots = self._stage_slot_registry.findByPipeline("outbound")
|
||
control_slots = self._stage_slot_registry.findByPipeline("control")
|
||
# 清理所有阶段槽位注册,避免下次 bootstrap 复用陈旧槽位
|
||
self._stage_slot_registry.clear()
|
||
await self._logger.info(
|
||
"管道已拆除(请求级,在途请求完成后无活跃实例,槽位注册已清理)",
|
||
inbound_slot_count=len(inbound_slots),
|
||
outbound_slot_count=len(outbound_slots),
|
||
control_slot_count=len(control_slots),
|
||
)
|
||
|
||
async def _stopTransportManager(self) -> None:
|
||
"""停止传输引擎管理器。
|
||
|
||
调用 ``TransportManager.stop()`` 优雅停止所有入站传输 Worker
|
||
(Puller/Stream),停止接收新消息并释放连接资源。超时强制
|
||
进入下一步并告警。
|
||
"""
|
||
try:
|
||
await asyncio.wait_for(
|
||
self._transport_manager.stop(),
|
||
timeout=self._STOP_TRANSPORT_MANAGER_TIMEOUT,
|
||
)
|
||
except TimeoutError:
|
||
await self._handleStepTimeout("停止传输引擎管理器", self._STOP_TRANSPORT_MANAGER_TIMEOUT)
|
||
except Exception as e:
|
||
await self._logger.warn(
|
||
f"停止传输引擎管理器异常,强制进入下一步: {e}",
|
||
error_type=type(e).__name__,
|
||
error=str(e),
|
||
)
|
||
|
||
async def _stopPlugins(self) -> None:
|
||
"""按反向依赖拓扑停止渠道插件。
|
||
|
||
列出所有 ``STARTED`` 与 ``PAUSED`` 状态的插件,按依赖解析器计算反向
|
||
拓扑序(被依赖者后停止),逐个调用 ``stop``。单个插件停止超时或失败
|
||
记录告警并继续,不阻塞后续插件停止。
|
||
|
||
注:``PAUSED`` 状态插件同样需要调用 ``onStop`` 释放资源,否则其资源
|
||
无法被清理。
|
||
"""
|
||
started_plugins = self._plugin_registry.listPluginsByState(LifecycleState.STARTED)
|
||
paused_plugins = self._plugin_registry.listPluginsByState(LifecycleState.PAUSED)
|
||
plugins_to_stop = started_plugins + paused_plugins
|
||
if not plugins_to_stop:
|
||
await self._logger.info("无 STARTED/PAUSED 状态插件可停止")
|
||
return
|
||
|
||
# 计算反向拓扑序(被依赖者后停止)
|
||
manifests = [pm.manifest for pm in plugins_to_stop]
|
||
try:
|
||
sorted_manifests = self._resolver.resolve(manifests)
|
||
stop_order: list[ChannelManifest] = list(reversed(sorted_manifests))
|
||
except Exception as e:
|
||
# 解析失败常见原因:
|
||
# 1. 依赖项不在待停止列表中(如依赖插件已 FAILED,不在
|
||
# STARTED/PAUSED 列表中)→ resolve 抛 ValidationError
|
||
# 2. 清单间存在循环依赖 → resolve 抛 RuleViolationError
|
||
# 回退为列表反转序:无法保证严格拓扑序,但保证所有插件都会被停止
|
||
await self._logger.warn(
|
||
f"插件依赖解析失败,回退为列表反转序(可能原因:依赖项不在待停止列表中或存在循环依赖),"
|
||
f"无法保证严格拓扑序但所有插件都会被停止: {e}",
|
||
error=str(e),
|
||
)
|
||
stop_order = list(reversed(manifests))
|
||
|
||
await self._logger.info(
|
||
f"开始停止 {len(stop_order)} 个插件(反向依赖拓扑序)",
|
||
)
|
||
for manifest in stop_order:
|
||
await self._stopSinglePlugin(manifest)
|
||
|
||
async def _stopSinglePlugin(self, manifest: ChannelManifest) -> None:
|
||
"""停止单个插件,超时或失败记录告警并继续。
|
||
|
||
Args:
|
||
manifest: 渠道清单。
|
||
"""
|
||
try:
|
||
result: LifecycleResult = await asyncio.wait_for(
|
||
self._plugin_lifecycle.stop(manifest.id),
|
||
timeout=self._STOP_PLUGIN_TIMEOUT,
|
||
)
|
||
# 将内置 asyncio.TimeoutError 翻译为契约 OperationTimeoutError 记录结构化告警
|
||
# (硬约束:所有模块必须使用自定义异常类而非原生异常)
|
||
except TimeoutError:
|
||
await self._handleStepTimeout(
|
||
f"插件停止: plugin_id={manifest.id}",
|
||
self._STOP_PLUGIN_TIMEOUT,
|
||
)
|
||
return
|
||
except Exception as e:
|
||
await self._logger.warn(
|
||
f"插件停止异常,强制中止: plugin_id={manifest.id}, error={e}",
|
||
plugin_id=manifest.id,
|
||
error_type=type(e).__name__,
|
||
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 _closeDrivenAdapters(self) -> None:
|
||
"""关闭被驱动适配器。
|
||
|
||
遍历所有已加载插件注册的 ``DrivenAdapters``,调用 ``close()``
|
||
释放数据库会话、连接池等资源。单个适配器关闭异常不中断其他
|
||
适配器关闭,记录告警后继续。超时强制进入下一步并告警。
|
||
"""
|
||
try:
|
||
await asyncio.wait_for(
|
||
self._closeAllDrivenAdapters(),
|
||
timeout=self._CLOSE_DRIVEN_TIMEOUT,
|
||
)
|
||
# 将内置 asyncio.TimeoutError 翻译为契约 OperationTimeoutError 记录结构化告警
|
||
# (硬约束:所有模块必须使用自定义异常类而非原生异常)
|
||
except TimeoutError:
|
||
await self._handleStepTimeout("关闭被驱动适配器", self._CLOSE_DRIVEN_TIMEOUT)
|
||
except Exception as e:
|
||
await self._logger.warn(
|
||
f"关闭被驱动适配器异常,强制进入下一步: {e}",
|
||
error_type=type(e).__name__,
|
||
error=str(e),
|
||
)
|
||
|
||
async def _closeAllDrivenAdapters(self) -> None:
|
||
"""关闭所有被驱动适配器,按 §15.2 顺序:先应用级,再插件级(INF-017)。
|
||
|
||
应用级被驱动适配器(``cache_port`` / ``persistence_port`` /
|
||
``queue_port`` / ``config_port``)在 ``HostBootstrap`` 构造时创建,
|
||
跨请求共享,需在关停时显式关闭以释放 Redis 连接、DB 会话等资源。
|
||
插件被驱动适配器由 ``PluginRegistry`` 管理,通过
|
||
``listPluginAdapters`` 遍历关闭。
|
||
|
||
单个适配器关闭异常记录告警并继续,确保后续适配器仍能关闭(避免
|
||
资源泄漏,INV-9 回退路径完整性)。
|
||
"""
|
||
# 1. 关闭应用级被驱动适配器(cache / persistence / queue / config)
|
||
for adapter in self._app_driven_adapters:
|
||
try:
|
||
if not isinstance(adapter, CloseablePort):
|
||
await self._logger.info(
|
||
"应用级被驱动适配器未实现 CloseablePort,跳过",
|
||
adapter_type=type(adapter).__name__,
|
||
)
|
||
continue
|
||
await adapter.aclose()
|
||
except Exception as exc:
|
||
await self._logger.warn(
|
||
"shutdown: close app driven adapter failed, continue",
|
||
adapter_type=type(adapter).__name__,
|
||
error_type=type(exc).__name__,
|
||
error=str(exc),
|
||
)
|
||
continue
|
||
await self._logger.info(
|
||
"应用级被驱动适配器已关闭",
|
||
adapter_type=type(adapter).__name__,
|
||
)
|
||
|
||
# 2. 关闭插件被驱动适配器(保持原有逻辑)
|
||
adapters_list = self._plugin_registry.listPluginAdapters()
|
||
for channel_type, adapters in adapters_list:
|
||
try:
|
||
await adapters.close()
|
||
await self._logger.info(
|
||
f"被驱动适配器已关闭: channel_type={channel_type}",
|
||
channel_type=channel_type,
|
||
)
|
||
except Exception as e:
|
||
await self._logger.warn(
|
||
f"关闭被驱动适配器失败,继续关闭其他: channel_type={channel_type}, error={e}",
|
||
channel_type=channel_type,
|
||
error=str(e),
|
||
)
|
||
if not adapters_list:
|
||
await self._logger.info("无被驱动适配器需要关闭")
|
||
|
||
async def _releaseCoreResources(self) -> None:
|
||
"""释放领域核心资源。
|
||
|
||
按启动的反向顺序取消后台任务(传输引擎管理器 / Outbox 恢复扫描 /
|
||
配对过期扫描 / 审计日志保留期清理 / 失败插件重载),记录核心资源
|
||
释放状态。超时强制进入下一步并告警。
|
||
"""
|
||
try:
|
||
await asyncio.wait_for(
|
||
self._releaseCoreResourcesInternal(),
|
||
timeout=self._RELEASE_CORE_TIMEOUT,
|
||
)
|
||
# 将内置 asyncio.TimeoutError 翻译为契约 OperationTimeoutError 记录结构化告警
|
||
# (硬约束:所有模块必须使用自定义异常类而非原生异常)
|
||
except TimeoutError:
|
||
await self._handleStepTimeout("释放核心资源", self._RELEASE_CORE_TIMEOUT)
|
||
except Exception as e:
|
||
await self._logger.warn(
|
||
f"释放核心资源异常,强制进入下一步: {e}",
|
||
error_type=type(e).__name__,
|
||
error=str(e),
|
||
)
|
||
|
||
async def _releaseCoreResourcesInternal(self) -> None:
|
||
"""释放核心资源内部实现。
|
||
|
||
惰性读取 ``host_bootstrap`` 上的后台任务引用(``bootstrap()`` 执行
|
||
后才填充),按 **启动的反向顺序** 取消并等待未完成的任务以释放资源
|
||
(后启动的先取消,符合"关停 = 启动的反向"原则)。``HostShutdown``
|
||
可能在 ``bootstrap()`` 之前创建,因此必须在关停时而非构造时读取
|
||
任务引用。
|
||
|
||
启动顺序:outbox → pairing → audit_log_retention → plugin_reload → transport_manager
|
||
关停顺序:transport_manager → plugin_reload → audit_log_retention → pairing → outbox
|
||
"""
|
||
# 按启动的反向顺序取消后台任务(后启动的先取消)
|
||
await self._cancelBackgroundTask(
|
||
self._host_bootstrap.transportManagerTask,
|
||
"传输引擎管理器任务",
|
||
)
|
||
await self._cancelBackgroundTask(
|
||
self._host_bootstrap.pluginReloadTask,
|
||
"失败插件重载任务(FR-36)",
|
||
)
|
||
await self._cancelBackgroundTask(
|
||
self._host_bootstrap.auditLogRetentionScanTask,
|
||
"审计日志保留期清理任务(FR-34)",
|
||
)
|
||
await self._cancelBackgroundTask(
|
||
self._host_bootstrap.pairingScanTask,
|
||
"配对过期扫描任务(FR-33)",
|
||
)
|
||
await self._cancelBackgroundTask(
|
||
self._host_bootstrap.outboxScanTask,
|
||
"Outbox 恢复扫描任务",
|
||
)
|
||
await self._logger.info("领域核心资源已释放")
|
||
|
||
async def _cancelBackgroundTask(
|
||
self,
|
||
task: asyncio.Task[None] | None,
|
||
name: str,
|
||
) -> None:
|
||
"""取消并等待单个后台任务完成。
|
||
|
||
若任务为 ``None``(``bootstrap()`` 未执行到对应阶段)或已完成,
|
||
直接跳过。取消后等待任务真正终止,``CancelledError`` 视为正常
|
||
取消结果,其余异常记录告警后继续。
|
||
|
||
Args:
|
||
task: 后台任务引用,可能为 ``None``。
|
||
name: 任务名称,用于日志标识。
|
||
"""
|
||
if task is None or task.done():
|
||
return
|
||
task.cancel()
|
||
try:
|
||
await task
|
||
except asyncio.CancelledError:
|
||
pass
|
||
except Exception as e:
|
||
await self._logger.warn(
|
||
f"{name}取消异常: {e}",
|
||
error=str(e),
|
||
)
|
||
await self._logger.info(f"{name}已取消")
|