From 9bdd31292829fede00671e901fe5b21f07fc7fbf Mon Sep 17 00:00:00 2001 From: iomgaa Date: Mon, 17 Aug 2026 12:26:37 -0400 Subject: [PATCH] fix: check the meta key budget before scanning every key The key-count cap exists to catch a whole request body dumped into meta. That is exactly the shape where the per-key regex runs tens of thousands of times before the real reason surfaces, so the cheap check goes first. Also correct two stale docstrings: postgres.py still claimed 22 columns (it is 24), and _canonical_meta_json promised to raise on non-finite floats. It is evaluated inside _record's degradation try, so the real outcome is a warning plus a dropped row -- never an error the caller sees. What the gate actually buys us is the SQLite side, whose meta is a TEXT column that would happily store a literal NaN. --- src/polygateway/middleware/telemetry.py | 9 +++++++-- src/polygateway/telemetry/postgres.py | 2 +- src/polygateway/types.py | 6 ++++-- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/polygateway/middleware/telemetry.py b/src/polygateway/middleware/telemetry.py index 6143668..335a495 100644 --- a/src/polygateway/middleware/telemetry.py +++ b/src/polygateway/middleware/telemetry.py @@ -42,8 +42,13 @@ def _canonical_meta_json(meta: Mapping[str, Any]) -> str: `allow_nan=False` 是**第二道闸**(主防线是 `types.validate_caller_dimensions` 在公共入口的校验): `json.dumps` 默认把 `nan` 写成裸 `NaN` 字面量,那不是合法 - JSON,PG 的 JSONB 会拒收;而写入失败会被 `_record` 的降级 try 吞成 warning, - 等于把调用方的输入错误转化成静默丢遥测。宁可在这里显式抛。 + JSON。这道闸真正的价值在 **SQLite 侧**——PG 的 JSONB 本来就会拒收 `NaN`,而 + SQLite 的 `meta` 是 TEXT 列**不做任何 JSON 校验**,没有这道闸就会把 `NaN` + 这种非法 JSON 静默存进去,污染后续一切按 JSON 解析 meta 的分析。 + + 注意它抛出的 `ValueError` **不会外泄给调用方**: 本函数在 `_record` 的降级 + `try` 内被求值,异常会被那里的 `except Exception` 接住 → 落 warning、整行 + 遥测丢弃。即入口失守时的真实结果是"警告 + 丢一行",不是"报错给调用方"。 """ if not meta: return "{}" diff --git a/src/polygateway/telemetry/postgres.py b/src/polygateway/telemetry/postgres.py index bd99511..3f35d96 100644 --- a/src/polygateway/telemetry/postgres.py +++ b/src/polygateway/telemetry/postgres.py @@ -6,7 +6,7 @@ ① 结构性失败 → warning 一次后永久降级(所有写入短路); ② 运行时单条写失败 → 逐条 warning 丢弃,不降级不重试(连接抖动由 asyncpg 池自恢复;避免浸泡开头一次抖动导致后续全程失遥测)。 -构造不连库(lazy),22 列 schema 与 SQLite 版同名同序。 +构造不连库(lazy),24 列 schema 与 SQLite 版同名同序。 **"结构性"的判据是「确定写不进去」,不是「初始化时出过错」**(issue #9): 只有建池失败(重试要在业务路径上内联吞掉 connect 超时)与"表确定不存在 diff --git a/src/polygateway/types.py b/src/polygateway/types.py index f67d485..bcb5180 100644 --- a/src/polygateway/types.py +++ b/src/polygateway/types.py @@ -115,6 +115,10 @@ def _validate_tenant_id(tenant_id: str | None, origin: str) -> None: def _validate_meta_keys(meta: Mapping[str, Any], origin: str) -> None: """键形态与数量;键集合被假定为低基数且稳定,故收紧到 OTel semconv 字符集。""" + # 数量闸先于逐键校验: 这道闸要防的正是"整个请求体被塞进 meta"的形态, + # 那时逐键正则会先跑上万次才报出真正的原因,拖慢的恰是出错路径 + if len(meta) > _META_MAX_KEYS: + raise ValueError(f"{origin} 的 meta 键数超限(上限 {_META_MAX_KEYS}): {len(meta)}") for key in meta: if not isinstance(key, str): raise ValueError(f"{origin} 的 meta 键必须是 str: {key!r}") @@ -127,8 +131,6 @@ def _validate_meta_keys(meta: Mapping[str, Any], origin: str) -> None: raise ValueError( f"{origin} 的 meta 键 {key!r} 不合法: 只允许小写字母/数字/下划线/点,长度 1-64" ) - if len(meta) > _META_MAX_KEYS: - raise ValueError(f"{origin} 的 meta 键数超限(上限 {_META_MAX_KEYS}): {len(meta)}") def _validate_meta_values(meta: Mapping[str, Any], origin: str) -> None: