feat: unlock redis/postgres backend config with assembly guards

This commit is contained in:
2026-07-21 00:18:42 -04:00
parent f9677fbd8e
commit acc1bcc18a
2 changed files with 107 additions and 12 deletions
+36 -6
View File
@@ -105,7 +105,9 @@ class GatewaySettings:
cache_ttl_s: int | None
telemetry_backend: str
telemetry_sqlite_path: str | None
telemetry_pg_dsn: str | None
redis_url: str | None
pricing_path: str | None
structured_max_retries: int
lease_ttl_s: float
@@ -139,6 +141,7 @@ class GatewaySettings:
**_load_pgw(env),
)
_guard_lease(settings)
_guard_stall(settings)
return settings
@@ -214,12 +217,20 @@ def _load_breaker(
)
if concurrency > 0:
threshold = max(threshold, concurrency * 2)
slowest = max(s.timeout_s for s in sources)
probe_floor = slowest + 5.0 # CHS container.py:274-275: 最慢调用 + 清理宽限
probe = _first(env, f"{scope}__BREAKER__PROBE_TTL_S")
if probe is not None:
probe_ttl_s = float(_cast(probe[1], "float", probe[0]))
# 装配守卫(M2 设计 §3): 探针租约必须撑过一次最慢调用,否则半开探针在途即被接管
if probe_ttl_s < probe_floor:
raise ValueError(
f"probe_ttl_s({probe_ttl_s})须 ≥ 最大源 timeout_s + 5({probe_floor});"
f"调大 {probe[0]} 或调小源超时"
)
else:
# 派生规则: 探针租约须撑过一次最慢调用,且不短于冷却期
probe_ttl_s = max(2 * max(s.timeout_s for s in sources), cooldown_s)
# 派生规则: 探针租约须撑过一次最慢调用,且不短于冷却期(第三项保证守卫恒成立)
probe_ttl_s = max(2 * slowest, cooldown_s, probe_floor)
return BreakerConfig(fail_threshold=threshold, cooldown_s=cooldown_s, probe_ttl_s=probe_ttl_s)
@@ -251,15 +262,15 @@ def _load_pgw(env: Mapping[str, str]) -> dict[str, object]:
breaker_backend = _load_choice(
env, "PGW_BREAKER_BACKEND", frozenset({"memory", "redis"}), "memory"
)
if "redis" in (limiter_backend, breaker_backend):
raise ValueError("限流/熔断 Redis 后端在 M2 交付;M1 仅支持 memory")
_, cache_backend = _require(env, "PGW_CACHE_BACKEND")
_, telemetry_backend = _require(env, "PGW_TELEMETRY_BACKEND")
if cache_backend not in ("redis", "memory", "none"):
raise ValueError(f"PGW_CACHE_BACKEND 非法值 {cache_backend!r}")
if telemetry_backend not in ("sqlite", "none"):
raise ValueError(f"PGW_TELEMETRY_BACKEND 非法值 {telemetry_backend!r}(postgres 在 M2)")
if telemetry_backend not in ("sqlite", "postgres", "none"):
raise ValueError(f"PGW_TELEMETRY_BACKEND 非法值 {telemetry_backend!r}")
redis_url = env.get("REDIS_URL") or None
if "redis" in (limiter_backend, breaker_backend) and redis_url is None:
raise ValueError("缺关键配置: 限流/熔断后端取 redis 需设置 REDIS_URL")
return {
"limiter_backend": limiter_backend,
"breaker_backend": breaker_backend,
@@ -269,12 +280,21 @@ def _load_pgw(env: Mapping[str, str]) -> dict[str, object]:
"telemetry_sqlite_path": _require(env, "PGW_TELEMETRY_SQLITE_PATH")[1]
if telemetry_backend == "sqlite"
else None,
"telemetry_pg_dsn": _load_pg_dsn(env) if telemetry_backend == "postgres" else None,
"redis_url": redis_url,
"pricing_path": env.get("PGW_PRICING_PATH") or None,
"structured_max_retries": _load_structured_retries(env),
"lease_ttl_s": _load_lease_ttl(env),
}
def _load_pg_dsn(env: Mapping[str, str]) -> str:
"""读取 Postgres DSN 并剥 SQLAlchemy 风格驱动后缀(asyncpg 不认 `+driver`)。"""
_, dsn = _require(env, "PGW_TELEMETRY_PG_DSN")
scheme, sep, rest = dsn.partition("://")
return f"{scheme.partition('+')[0]}{sep}{rest}"
def _load_cache_keys(
env: Mapping[str, str], cache_backend: str, redis_url: str | None
) -> dict[str, object]:
@@ -308,6 +328,16 @@ def _guard_lease(settings: GatewaySettings) -> None:
)
def _guard_stall(settings: GatewaySettings) -> None:
"""装配守卫: stall 窗口须 ≥ 最慢源 TTFT 上限,防把正常慢首包误判为卡死(ARCH §7.3)。"""
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"
)
def _load_lease_ttl(env: Mapping[str, str]) -> float:
found = _first(env, "PGW_LEASE_TTL_S")
return float(_cast(found[1], "float", found[0])) if found else _DEFAULT_LEASE_TTL_S