fix(config): 装配守卫挪进 __post_init__,任何构造路径都生效
_guard_lease 与 _guard_stall 原先只写在 GatewaySettings.from_env 里, 而 CLAUDE.md §4.5 规定装配有两条官方路径。结果是走 from_settings() 能装出 一个违反类不变量的 settings —— 这个类的 docstring 声称「构造经 from_env 聚合 并通过全部守卫」,但它可以合法地存在于自己声称不可能的状态。 守卫挪进 __post_init__,与同族的 SourceConfig 一致。放构造期而不是在每个工厂里 各加一行:三个 client(Gateway/Ocr/Embedding)各有两个工厂,共六个入口, 挂构造期是一处,挂工厂是六处要保持同步——那正是「每个调用方各维护一份副本」 的毛病,只是挪进了库里。OcrSettings 与 EmbeddingSettings 都包着一个 GatewaySettings,因此一并覆盖。 两个守卫的报错文案改为点字段名,环境变量键降为补充信息。守卫现在每次构造都跑, 而走 from_settings 的调用方从没设过那些键,让他「调大 PGW_LEASE_TTL_S」 是句没法执行的建议。 行为收紧:直接构造或 dataclasses.replace 出非法组合,现在构造期就抛 ValueError, 而不是留到运行时表现为租约先于请求过期、或正常慢首包被误判为卡死。 经 from_env 装配的调用方不受影响——那条路本来就跑这两个守卫。 测试:新增 TestGuardsRunOnEveryConstruction 四条(先失败 3 条后全过)。 既有 447 passed / 34 skipped 全部保持,无回归;ruff check、ruff format --check、 lint-imports 三门均通过。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -88,7 +88,15 @@ def _require(env: Mapping[str, str], *keys: str) -> tuple[str, str]:
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class GatewaySettings:
|
||||
"""一个 scope 的完整装配配置;构造经 from_env 聚合并通过全部守卫。"""
|
||||
"""一个 scope 的完整装配配置;**任何**构造路径都通过全部守卫。
|
||||
|
||||
守卫在 `__post_init__` 里跑,而不是在 `from_env` 里。原因是装配有两条官方路径
|
||||
(CLAUDE.md §4.5: `from_env()` 与 `from_settings()`),守卫只挂在其中一条上的话,
|
||||
另一条就能装出违反本类不变量的配置 —— 类会存在于它自己声称不可能的状态。
|
||||
放构造期还有一个好处: 三个 client(Gateway/Ocr/Embedding)各有两个工厂,
|
||||
共六个入口,挂在这里是一处,挂在工厂里是六处要保持同步。
|
||||
与 `SourceConfig.__post_init__` 的做法一致。
|
||||
"""
|
||||
|
||||
scope: str
|
||||
sources: tuple[SourceConfig, ...]
|
||||
@@ -111,6 +119,10 @@ class GatewaySettings:
|
||||
structured_max_retries: int
|
||||
lease_ttl_s: float
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
_guard_lease(self)
|
||||
_guard_stall(self)
|
||||
|
||||
@classmethod
|
||||
def from_env(
|
||||
cls,
|
||||
@@ -140,8 +152,7 @@ class GatewaySettings:
|
||||
quota_full=_load_choice(env, f"{scope_u}__QUOTA_FULL", _QUOTA_FULL, "wait"),
|
||||
**_load_pgw(env),
|
||||
)
|
||||
_guard_lease(settings)
|
||||
_guard_stall(settings)
|
||||
# 守卫已在 __post_init__ 里跑过,这里不再重复调用。
|
||||
return settings
|
||||
|
||||
|
||||
@@ -340,22 +351,33 @@ def _load_structured_retries(env: Mapping[str, str]) -> int:
|
||||
|
||||
|
||||
def _guard_lease(settings: GatewaySettings) -> None:
|
||||
"""装配守卫: 调用超时须 ≤ permit 租约 TTL,防租约先于请求过期(ARCH §7.3)。"""
|
||||
"""装配守卫: 调用超时须 ≤ permit 租约 TTL,防租约先于请求过期(ARCH §7.3)。
|
||||
|
||||
文案点字段名而不是只点环境变量键: 守卫在每次构造时都跑,而走 `from_settings`
|
||||
的调用方从没设过那些键,让他去"调大 PGW_LEASE_TTL_S"是句没法执行的建议。
|
||||
环境变量键作为补充信息附在后面,给走 `from_env` 的人用。
|
||||
"""
|
||||
slowest = max(s.timeout_s for s in settings.sources)
|
||||
if slowest > settings.lease_ttl_s:
|
||||
raise ValueError(
|
||||
f"源最大 timeout_s({slowest})超过 permit 租约 TTL({settings.lease_ttl_s});"
|
||||
f"调大 PGW_LEASE_TTL_S 或调小超时"
|
||||
f"源最大 timeout_s({slowest})超过 permit 租约 TTL(lease_ttl_s="
|
||||
f"{settings.lease_ttl_s});调大 lease_ttl_s 或调小源的 timeout_s"
|
||||
f"(走 from_env 时对应的键是 PGW_LEASE_TTL_S 与 {{SCOPE}}__{{PROVIDER}}__{{N}}__TIMEOUT_S)"
|
||||
)
|
||||
|
||||
|
||||
def _guard_stall(settings: GatewaySettings) -> None:
|
||||
"""装配守卫: stall 窗口须 ≥ 最慢源 TTFT 上限,防把正常慢首包误判为卡死(ARCH §7.3)。"""
|
||||
"""装配守卫: stall 窗口须 ≥ 最慢源 TTFT 上限,防把正常慢首包误判为卡死(ARCH §7.3)。
|
||||
|
||||
文案点字段名的理由同 `_guard_lease`。
|
||||
"""
|
||||
ttfts = [s.ttft_timeout_s for s in settings.sources if s.ttft_timeout_s is not None]
|
||||
if ttfts and settings.backpressure.stall_window_s < max(ttfts):
|
||||
raise ValueError(
|
||||
f"stall_window_s({settings.backpressure.stall_window_s})须 ≥ 最大源 "
|
||||
f"ttft_timeout_s({max(ttfts)});调大 BACKPRESSURE__STALL_WINDOW_S 或调小 TTFT"
|
||||
f"backpressure.stall_window_s({settings.backpressure.stall_window_s})须 ≥ 最大源 "
|
||||
f"ttft_timeout_s({max(ttfts)});调大 stall_window_s 或调小 ttft_timeout_s"
|
||||
f"(走 from_env 时对应的键是 {{SCOPE}}__BACKPRESSURE__STALL_WINDOW_S 与 "
|
||||
f"{{SCOPE}}__{{PROVIDER}}__{{N}}__TTFT_TIMEOUT_S)"
|
||||
)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user