fix: degrade terminal telemetry failures instead of masking domain errors
终态出口 `emit_terminal_once` 此前只让 `_record` 内的 except 兜住落库,而 诊断字段的提取(`_error_fields` → `_structured_detail` → `format_bounded_errors`) 在降级 try **之外**求值。下游经公共端口(自实现 `StructuredOutputStrategy` 或 transport)构造出 `ResultInvalidError(validation_errors=(非 str,))` 时,提取期 抛的 `TypeError` 会顶替调用方本该收到的领域异常——错误四分类被击穿(下游 `except ResultInvalidError` 落空),且 `claim_terminal()` 已消耗故终态行照样丢, 同时违反"遥测写失败降级不冒泡"。 改法与 RetryMW 的 attempt 出口(`retry.py::_emit`)同款: 把快照冻结与 await 整段包进 try,`CancelledError` 原样上抛、其余落一条 warning。终态行按已批准的 best effort(兜底命中时该次逻辑调用 0 条终态行,不补写)。异常类型校验与 `ResultInvalidError` 的既有设计均未改动。 顺带同步审查报告的 Minor 项: SQLite recorder docstring 26 → 36 字段、 research-wiki 索引重建、ARCHITECTURE 的 `sampling` 段落终态调用点口径, 并删除 `TelemetryMW` 迁移后无读取点的 `self._now` 死字段(保留形参, 避免平白打断既有装配写法)。
This commit is contained in:
@@ -679,7 +679,16 @@ async def emit_terminal_once(
|
||||
"""三个 client 共用的**终态唯一出口**: 去重 + 同步冻结快照 + best effort 写入。
|
||||
|
||||
去重由 `claim_terminal()` 承担(每逻辑调用至多一条终态行);`emitter is None`
|
||||
或已写过 → 直接返回。写入侧异常按既有降级只落 warning(在 `_record` 内)。
|
||||
或已写过 → 直接返回。
|
||||
|
||||
**降级范围包含诊断字段的提取与构建**,不只是写入那一步: `_record` 内的
|
||||
`except Exception` 只兜住落库,而 `_error_fields` / `canonical_sampling_json`
|
||||
在它**之外**求值——下游经公共端口(自实现 `StructuredOutputStrategy` 或
|
||||
transport)构造出的 `ResultInvalidError(validation_errors=(非 str,))` 会让提取期
|
||||
抛 `TypeError` 顶替调用方本该收到的领域异常,错误四分类被击穿且终态行照样丢。
|
||||
故在此整段兜底,与 RetryMW 的 attempt 出口(`retry.py::_emit`)同款写法。
|
||||
终态行按 best effort: 兜底命中时本次逻辑调用**没有**终态行(`claim_terminal()`
|
||||
已消耗,不补写、不重试——重写一遍只会把同一个提取期异常再抛一次)。
|
||||
|
||||
**`CancelledError` 原样传播**(取消优先,不 shield、不开后台任务): 这一次
|
||||
`await` 本身就是新的取消点,外部取消落在它上时调用方会看到 `CancelledError`
|
||||
@@ -688,15 +697,20 @@ async def emit_terminal_once(
|
||||
"""
|
||||
if emitter is None or not context.claim_terminal():
|
||||
return
|
||||
stats = context.snapshot()
|
||||
await emitter.emit_terminal_failure(
|
||||
request=request,
|
||||
call_id=str(uuid.uuid4()),
|
||||
error=error,
|
||||
operation=operation,
|
||||
stats=stats,
|
||||
class_prefixed_error=class_prefixed_error,
|
||||
)
|
||||
try:
|
||||
stats = context.snapshot()
|
||||
await emitter.emit_terminal_failure(
|
||||
request=request,
|
||||
call_id=str(uuid.uuid4()),
|
||||
error=error,
|
||||
operation=operation,
|
||||
stats=stats,
|
||||
class_prefixed_error=class_prefixed_error,
|
||||
)
|
||||
except asyncio.CancelledError:
|
||||
raise
|
||||
except Exception as exc:
|
||||
logger.warning("终态遥测记录失败(降级不冒泡): {}", exc)
|
||||
|
||||
|
||||
class TelemetryMW:
|
||||
@@ -711,7 +725,8 @@ class TelemetryMW:
|
||||
self, emitter: TelemetryEmitter, now: Callable[[], float] = time.monotonic
|
||||
) -> None:
|
||||
self._emitter = emitter
|
||||
self._now = now
|
||||
# `now` 自 1.3.5 起本类不再读取(终态行迁到公开边界后无耗时可测),但形参保留:
|
||||
# 删它会平白打断 `TelemetryMW(emitter, now=...)` 这一既有装配写法
|
||||
|
||||
async def __call__(self, request: ChatRequest, call_next: CallNext) -> LLMResponse:
|
||||
response = await call_next(request)
|
||||
|
||||
Reference in New Issue
Block a user