"""宿主关停编排器。 本模块是编排层的关停编排入口,按反向顺序释放渠道网关 资源。每步有超时,超时强制进入下一步并告警, 关停不得丢弃在途请求(除非超时强制终止)。 关停顺序: 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}已取消")