feat: add an optional per-call wall-clock deadline
Give one logical call an optional hard wall-clock boundary (issue #22). Leaving it unset keeps 1.3.5 behaviour verbatim: the timeout context is never entered when deadline_s is None. - new deadline.py: ensure_call_deadline() range check (None or a finite positive number; bool/0/nan/inf and out-of-range ints are rejected as ValueError so OverflowError never leaks) plus with_call_deadline(), which distinguishes an expiry from a TimeoutError raised by the body or its cleanup via a local-variable identity comparison rather than cm.expired() alone - new CallDeadlineExceeded: deliberately outside the four categories and not a GatewayUnavailableError, and carries no retry_after_s - new {SCOPE}__CALL_DEADLINE_S key, guarded on the env, direct construction and dataclasses.replace paths - three clients take a call_deadline_s constructor argument and a keyword-only per-call override on chat/embed/recognize_text/ parse_layout; None inherits the assembled value - validation runs before the awaitable is created, so an illegal value cannot strand an un-awaited coroutine - one embed call shares a single deadline across all of its batches - import-linter gains a polygateway.deadline layer - cover where the deadline lands: backoff sleep, admission polling, the structured re-ask ladder and embedding's batch loop, plus the empty-texts early return that stays outside it - cover what an expiry costs: exactly one terminal_failure row carrying error_type=CallDeadlineExceeded, a cancelled attempt row sharing its logical_call_id, cleanup that outlives the deadline (lower bound only) and an already-billed success being discarded - pin the injected clock as orthogonal: a 10^6 second jump never expires a call, yet total_latency_ms still reads that clock
This commit is contained in:
@@ -217,3 +217,23 @@ class GovernanceBackendError(GatewayUnavailableError):
|
||||
# 父类会把 message 覆写为 "{scope} 网关暂时不可用: {reason}",而各构造点
|
||||
# 携带的诊断串(如"限流后端 try_acquire 失败: ...")是排障主线索,必须保住
|
||||
self.args = (message,)
|
||||
|
||||
|
||||
class CallDeadlineExceeded(PolyGatewayError): # noqa: N818 — 设计 §9 人类批准的公共名
|
||||
"""调用方设定的整体调用期限到期; 不是网关不可用、也不是源故障。
|
||||
|
||||
刻意**不属**四分类、**不进** `SCOPE_REASONS`、**不继承** `GatewayUnavailableError`:
|
||||
它描述的是调用方自己的耐心边界, 与"对方怎么了"正交——按四分类之一上报会让
|
||||
下游的重试/换源/熔断逻辑对着一次本地超时做治理决策(库铁律「错误分类驱动」)。
|
||||
|
||||
也刻意**没有** `retry_after_s`: 期限到期不含"何时可再试"的信息, 给 `0.0`
|
||||
会按既定语义指示下游立刻重打一条可能已经饱和的通道。
|
||||
|
||||
**到期不等于未产出、未计费**: 期限治理的是等待, 在途请求可能已经发出、
|
||||
已被上游计费, 清理仍在 `finally` 里完成, 故返回时刻 = 期限 + 清理耗时。
|
||||
"""
|
||||
|
||||
def __init__(self, *, scope: str, deadline_s: float) -> None:
|
||||
super().__init__(f"{scope} 调用期限 {deadline_s}s 到期")
|
||||
self.scope = scope
|
||||
self.deadline_s = deadline_s
|
||||
|
||||
Reference in New Issue
Block a user