fix: normalise scope and blank strings on the construction path too

The verifier found four more env-only behaviours of the same class the branch
was already fixing. The worst is scope: it goes straight into the Redis keys
(pgw:limit:{scope}, pgw:gate:{scope}), so one process using from_env("LLM")
and another constructing scope="LLM" by hand split the rate limit and breaker
state across two namespaces, each tracking its own quota, with no error.

Blank redis_url and pricing_path now collapse to None as from_env has always
done, so they fall into the required-field checks instead of reaching the redis
client as an unparseable URL. EmbeddingSettings gains the __post_init__ it never
had, moving its batch_size and expected_dim checks off the from_env-only path.

Also adds the cache backend whitelist test that mutation testing showed missing.
This commit is contained in:
2026-07-30 02:15:32 -04:00
parent c9fdff9d55
commit 726f26d8bd
4 changed files with 110 additions and 3 deletions
+26
View File
@@ -129,6 +129,7 @@ class GatewaySettings:
lease_ttl_s: float
def __post_init__(self) -> None:
self._normalize()
self._validate_identity()
self._validate_backends()
self._validate_cache()
@@ -137,6 +138,24 @@ class GatewaySettings:
self._validate_stall()
self._validate_probe()
def _normalize(self) -> None:
"""把 `from_env` 一直在做的规范化补到构造路上,两条路必须产出同一个值。
`scope` 最要紧: 它直接进 Redis key(`pgw:limit:{scope}:…`/`pgw:gate:{scope}:…`)。
一个进程走 `from_env("LLM")` 拿到 "llm"、另一个直接构造传 "LLM",同一逻辑
scope 的限流与熔断状态会分裂到两套命名空间,各记各的,治理静默失效且不报错。
空串归 None 同理: 留着空串会骗过 `is None` 判断,把错误推迟到 redis 客户端
抛连接串解析异常。`telemetry_pg_dsn` 的驱动后缀因为要看 backend 且需告警,
规范化留在 `_validate_telemetry`。
"""
normalized_scope = self.scope.strip().lower()
if normalized_scope != self.scope:
object.__setattr__(self, "scope", normalized_scope)
for field in ("redis_url", "pricing_path"):
if getattr(self, field) == "":
object.__setattr__(self, field, None)
def _validate_identity(self) -> None:
"""本类自身字段的基本域: 空 scope 会污染遥测与缓存命名空间;零源必然选源失败。"""
if not self.scope.strip():
@@ -473,6 +492,13 @@ class EmbeddingSettings:
normalize: bool = False
expected_dim: int | None = None
def __post_init__(self) -> None:
"""自身字段的域校验;内嵌的 gateway 由 `GatewaySettings.__post_init__` 自己把关。"""
if self.batch_size < 1:
raise ValueError(f"EmbeddingSettings.batch_size 必须 ≥ 1: {self.batch_size}")
if self.expected_dim is not None and self.expected_dim < 1:
raise ValueError(f"EmbeddingSettings.expected_dim 必须 ≥ 1: {self.expected_dim}")
@classmethod
def from_env(
cls,