1. 移除所有适配器文件中多余的空导入行 2. 调整ValidationError继承,移除不必要的ValueError继承 3. 修正多处ChannelType使用方式,从.value改为直接使用枚举实例 4. 优化飞书插件部分硬编码渠道类型为枚举实例 5. 更新wechat_ilink插件清单与适配器配置 6. 新增飞书目录适配器缓存清理支持判断与iLink生命周期适配器凭据轮换支持判断 7. 优化配置处理器历史查询逻辑,区分键不存在与无历史记录场景
249 lines
10 KiB
Python
249 lines
10 KiB
Python
"""渠道主动探测器。
|
||
|
||
实现 ``ChannelProbe``,主动调用渠道 API 探测可用性,不得仅返回缓存状态。
|
||
探测通过可探测适配器(``ProbeableAdapter``)的 ``probe()`` 方法执行,
|
||
超时 30s,结果映射为契约层 ``ProbeResult``。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import asyncio
|
||
import time
|
||
|
||
from yuxi.channels.contract.dtos.channel import ChannelType
|
||
from yuxi.channels.contract.dtos.health import AdapterProbeOutcome, ProbeResult
|
||
from yuxi.channels.contract.errors.server import OperationTimeoutError
|
||
from yuxi.channels.contract.plugin.adapters import ProbeableAdapter
|
||
from yuxi.channels.contract.plugin.manifest import PluginManifest
|
||
from yuxi.channels.contract.ports.driven.cache_port import CachePort
|
||
from yuxi.channels.contract.ports.driven.logger_port import LoggerPort
|
||
from yuxi.channels.contract.ports.driven.persistence_health_port import PersistenceHealthPort
|
||
from yuxi.channels.contract.ports.driven.queue_port import QueuePort
|
||
from yuxi.channels.core.registry.plugin_registry import PluginRegistry
|
||
|
||
__all__ = ["ChannelProbe", "ProbeableAdapter", "AdapterProbeOutcome"]
|
||
|
||
|
||
class ChannelProbe:
|
||
"""渠道主动探测器。
|
||
|
||
FR-35。主动调用渠道 API 探测可用性,
|
||
不得仅返回缓存状态。探测可触发副作用(需明确标识)。
|
||
|
||
探测流程:
|
||
1. 检查下游依赖(DB / Redis / Worker),任一异常返回 ``healthy=False``。
|
||
下游检查与适配器探测均受 ``PROBE_TIMEOUT`` 超时保护。
|
||
2. 按清单的渠道类型从 ``PluginRegistry`` 动态查找可探测适配器
|
||
(``ProbeableAdapter``)。插件未注册可探测适配器时返回
|
||
``healthy=False`` 且 message 标注"渠道未实现主动探测",
|
||
以区分"不健康"与"不支持探测"。
|
||
3. 通过 ``asyncio.wait_for`` 调用适配器 ``probe()``,超时 30s。
|
||
4. 将适配器探测结果映射为契约层 ``ProbeResult``。
|
||
|
||
关键约束:
|
||
- **不得** 仅返回缓存状态,必须主动调用渠道 API。
|
||
- 探测超时抛出 ``TimeoutError``(HTTP 504,FR-35),由上层
|
||
``unified_error_handler`` 统一处理。
|
||
- 探测异常返回 ``healthy=False`` 的 ``ProbeResult``,不抛异常。
|
||
- 下游依赖(DB / Redis / Worker)异常时返回 ``healthy=False``,
|
||
避免下游故障被适配器探测掩盖。
|
||
"""
|
||
|
||
PROBE_TIMEOUT = 30.0
|
||
"""探测超时时间(秒)。"""
|
||
|
||
def __init__(
|
||
self,
|
||
plugin_registry: PluginRegistry,
|
||
persistence_port: PersistenceHealthPort,
|
||
cache_port: CachePort,
|
||
queue_port: QueuePort,
|
||
logger: LoggerPort,
|
||
) -> None:
|
||
"""初始化渠道主动探测器。
|
||
|
||
参数:
|
||
plugin_registry: 插件注册表,用于动态查找渠道的可探测适配器。
|
||
适配器由插件通过 ``PluginHost.registerAdapter("probeable", ...)``
|
||
注册,``ChannelProbe`` 每次探测时从注册表动态查找,保证插件
|
||
加载/卸载后适配器索引自动更新。
|
||
persistence_port: 持久化被驱动端口,用于探测前检查数据库可用性。
|
||
cache_port: 缓存被驱动端口,用于探测前检查缓存可用性。
|
||
queue_port: 队列被驱动端口,用于探测前检查 Worker 状态。
|
||
logger: 日志被驱动端口,用于记录探测过程。
|
||
"""
|
||
self._registry = plugin_registry
|
||
self._persistence_port = persistence_port
|
||
self._cache_port = cache_port
|
||
self._queue_port = queue_port
|
||
self._logger = logger
|
||
|
||
async def probe(self, manifest: PluginManifest) -> ProbeResult:
|
||
"""主动探测渠道 API 可用性。
|
||
|
||
参数:
|
||
manifest: 插件清单,提供渠道类型信息。
|
||
|
||
返回:
|
||
探针结果。``account_id`` 为空字符串(探测为渠道类型级别,
|
||
不绑定具体账户);``healthy`` 反映探测时刻的渠道真实状态。
|
||
"""
|
||
channel_type = manifest.manifest.channel_type
|
||
start = time.monotonic()
|
||
|
||
# 下游依赖检查 + 适配器探测均受 PROBE_TIMEOUT 超时保护,
|
||
# 避免下游 ping/worker_status 挂起导致探测请求无限期阻塞。
|
||
try:
|
||
return await asyncio.wait_for(
|
||
self._probeInternal(channel_type, start),
|
||
timeout=self.PROBE_TIMEOUT,
|
||
)
|
||
except TimeoutError as exc:
|
||
latency_ms = int((time.monotonic() - start) * 1000)
|
||
await self._logger.warn(
|
||
"渠道探测超时",
|
||
channel_type=channel_type,
|
||
timeout=str(self.PROBE_TIMEOUT),
|
||
latency_ms=str(latency_ms),
|
||
)
|
||
raise OperationTimeoutError(
|
||
int(self.PROBE_TIMEOUT * 1000),
|
||
message=f"probe timed out after {self.PROBE_TIMEOUT}s",
|
||
) from exc
|
||
|
||
async def _probeInternal(self, channel_type: ChannelType, start: float) -> ProbeResult:
|
||
"""执行探测内部逻辑(下游检查 + 适配器探测)。
|
||
|
||
参数:
|
||
channel_type: 渠道类型。
|
||
start: 探测开始时间(``time.monotonic()``),用于计算延迟。
|
||
|
||
返回:
|
||
探针结果。
|
||
"""
|
||
# 下游依赖检查:DB / Redis / Worker(队列)。任一异常时返回不健康,
|
||
# 避免下游故障被适配器探测掩盖。
|
||
try:
|
||
db_ok = await self._persistence_port.ping()
|
||
except Exception as exc:
|
||
await self._logger.warn(
|
||
"渠道探测下游检查:DB ping 异常",
|
||
channel_type=channel_type,
|
||
error=str(exc),
|
||
)
|
||
db_ok = False
|
||
if not db_ok:
|
||
return ProbeResult(
|
||
channel_type=channel_type,
|
||
account_id="",
|
||
healthy=False,
|
||
latency_ms=0,
|
||
message="下游依赖异常:数据库不可用",
|
||
)
|
||
|
||
# CachePort.ping 契约保证故障时返回 False 不抛异常,但实现层
|
||
# (RedisCacheAdapter)依赖 Redis 连接,连接级故障仍可能抛异常。
|
||
# 此处捕获兜底,与 DB ping 处理一致,避免探测被缓存故障穿透。
|
||
try:
|
||
redis_ok = await self._cache_port.ping()
|
||
except Exception as exc:
|
||
await self._logger.warn(
|
||
"渠道探测下游检查:Redis ping 异常",
|
||
channel_type=channel_type,
|
||
error=str(exc),
|
||
)
|
||
redis_ok = False
|
||
if not redis_ok:
|
||
return ProbeResult(
|
||
channel_type=channel_type,
|
||
account_id="",
|
||
healthy=False,
|
||
latency_ms=0,
|
||
message="下游依赖异常:缓存不可用",
|
||
)
|
||
|
||
try:
|
||
worker_status = await self._queue_port.getWorkerStatus()
|
||
except Exception as exc:
|
||
await self._logger.warn(
|
||
"渠道探测下游检查:Worker 状态查询异常",
|
||
channel_type=channel_type,
|
||
error=str(exc),
|
||
)
|
||
worker_status = None
|
||
# getWorkerStatus 返回 WorkerStatus DTO;查询异常或 available 非 True
|
||
# 均视为 Worker 停摆(与 HealthAggregator._aggregate 判定一致)。
|
||
if not (worker_status is not None and worker_status.available is True):
|
||
return ProbeResult(
|
||
channel_type=channel_type,
|
||
account_id="",
|
||
healthy=False,
|
||
latency_ms=0,
|
||
message="下游依赖异常:Worker 停摆",
|
||
)
|
||
|
||
# 从 PluginRegistry 动态查找可探测适配器:插件通过
|
||
# registerAdapter("probeable", ...) 注册 ProbeableAdapter,
|
||
# 此处按 channel_type 索引。未注册时返回"渠道未实现主动探测",
|
||
# 以区分"渠道不健康"与"渠道不支持探测"。
|
||
adapter = self._findProbeableAdapter(channel_type)
|
||
if adapter is None:
|
||
return ProbeResult(
|
||
channel_type=channel_type,
|
||
account_id="",
|
||
healthy=False,
|
||
latency_ms=int((time.monotonic() - start) * 1000),
|
||
message="渠道未实现主动探测,状态基于插件生命周期",
|
||
)
|
||
|
||
try:
|
||
outcome = await adapter.probe()
|
||
except Exception as e:
|
||
latency_ms = int((time.monotonic() - start) * 1000)
|
||
await self._logger.warn(
|
||
"渠道探测异常",
|
||
channel_type=channel_type,
|
||
error=str(e),
|
||
latency_ms=str(latency_ms),
|
||
)
|
||
return ProbeResult(
|
||
channel_type=channel_type,
|
||
account_id="",
|
||
healthy=False,
|
||
latency_ms=latency_ms,
|
||
message=f"探测异常: {e}",
|
||
)
|
||
|
||
latency_ms = outcome.latency_ms if outcome.latency_ms > 0 else int((time.monotonic() - start) * 1000)
|
||
await self._logger.info(
|
||
"渠道探测完成",
|
||
channel_type=channel_type,
|
||
healthy=str(outcome.is_available),
|
||
latency_ms=str(latency_ms),
|
||
)
|
||
return ProbeResult(
|
||
channel_type=channel_type,
|
||
account_id="",
|
||
healthy=outcome.is_available,
|
||
latency_ms=latency_ms,
|
||
message=outcome.reason,
|
||
)
|
||
|
||
def _findProbeableAdapter(self, channel_type: ChannelType) -> ProbeableAdapter | None:
|
||
"""从插件注册表查找指定渠道类型的可探测适配器。
|
||
|
||
遍历 ``PluginRegistry.listPluginAdapters()`` 返回的
|
||
``(channel_type, DrivenAdapters)`` 列表,匹配渠道类型后从
|
||
``DrivenAdapters.probeable_adapters`` 取首个适配器。
|
||
|
||
参数:
|
||
channel_type: 渠道类型。
|
||
|
||
返回:
|
||
可探测适配器实例,未注册时返回 ``None``。
|
||
"""
|
||
for ct, adapters in self._registry.listPluginAdapters():
|
||
if ct == channel_type and adapters.probeable_adapters:
|
||
return adapters.probeable_adapters[0]
|
||
return None
|