71e11c317f
- #3 六个消费视图 resolver 收拢 app/platform/config_views(单一定义点): resolve_email/push/ai_settings ← 各服务文件, resolve_contact_flags/ context_seed_map ← conversation, name_to_region_extras ← manager __init__ 内联取值(or None 语义原样)——函数体逐字搬入,服务层 import 之, 旧定义删;Phase 3 编辑器/试金石与该模块对接 - #2 限流 0 语义统一: conn_limiter.try_acquire 的 limit 与 chat_rest 的 rate_limit_per_hour 取值同款——0 或负 = 回退出厂默认(与姊妹键一致), 原 conn_limiter 0=全拒改为回退;chat_rest 取值抽 _resolve_rate_limit 纯函数供锚定 - #1 FakeAI 收敛进 conftest(超集: extract/evaluate/eval_calls),四份拷贝 删(test_lead_email_trigger/test_cc_integration/test_channel_hygiene/ test_lead_push_wiring——计划记三份,grep 实有四份;push_wiring 经 evaluate 参数注入原本地固定返回值,断言零改动) 全量 558 passed(554 + 新增 4: 单入口/内联等价/两处 0 语义) Co-Authored-By: Claude Code <noreply@anthropic.com>
63 lines
2.6 KiB
Python
63 lines
2.6 KiB
Python
"""WS 每 IP 并发连接上限(spec §6.1 P2)——进程内 (租户, IP) 计数闸。
|
|
|
|
动机: 平台化后单 IP 可对所有租户域名各开一圈 WS 耗尽单进程资源;
|
|
REST 消息接口已有的 rate_limit.py 管不到长连接握手。key 是
|
|
(tenant_slug, ip) 二元组——各租户名额独立,不共享预算。
|
|
|
|
约束与取舍(与 rate_limit.py 同一族):
|
|
- 单进程内存态, ``--workers 1`` 约束下准确; 多 worker 各自一份计数,
|
|
实际上限 = limit * workers。
|
|
- 纯同步 dict 操作、方法内无 await——单事件循环下 check-then-set
|
|
原子,无需锁;不得在多线程里使用。
|
|
- 计数归零即删 key, 长期运行不随历史 (租户, IP) 对积累内存;
|
|
release 未持有的 key 是安全 no-op(不出负数)。
|
|
- limit 可在 try_acquire 时按租户配置覆盖: 模块单例服务全部租户,而
|
|
ws_conn_limit 是 engine 组的租户级配置——按次传入让各租户差异实时
|
|
生效(含 registry_reload 后), 又不必维护"每租户一个 limiter 实例"
|
|
的第二层簿记(实例缓存还有配置改了不生效的滞留问题)。
|
|
"""
|
|
from typing import Optional
|
|
|
|
DEFAULT_WS_CONN_LIMIT = 10
|
|
|
|
|
|
class ConnectionLimiter:
|
|
"""每 (tenant, ip) 并发计数器: 满则 try_acquire 拒绝, release 归零清键。"""
|
|
|
|
def __init__(self, limit: int = DEFAULT_WS_CONN_LIMIT) -> None:
|
|
self._limit = limit
|
|
self._counts: dict[tuple[str, str], int] = {}
|
|
|
|
def try_acquire(self, tenant: str, ip: str, *,
|
|
limit: Optional[int] = None) -> bool:
|
|
"""未满则记账并返回 True; 已满返回 False(不记账)。
|
|
|
|
limit 按次覆盖构造默认; 0 或负 = 回退出厂默认(与姊妹键
|
|
rate_limit_per_hour 一致)——config_validation 已拒 0/负,此处
|
|
兜直写快照/旧库数据,不让坏值炸成全拒(Task 10 前 0=全拒)。
|
|
"""
|
|
cap = limit if limit and limit > 0 else self._limit
|
|
key = (tenant, ip)
|
|
n = self._counts.get(key, 0)
|
|
if n >= cap:
|
|
return False
|
|
self._counts[key] = n + 1
|
|
return True
|
|
|
|
def release(self, tenant: str, ip: str) -> None:
|
|
"""归还一个名额; 计数归零删 key; 未持有的 key 是安全 no-op。"""
|
|
key = (tenant, ip)
|
|
n = self._counts.get(key, 0)
|
|
if n <= 1:
|
|
self._counts.pop(key, None)
|
|
else:
|
|
self._counts[key] = n - 1
|
|
|
|
|
|
# app 级单例: chat.py 的 WS 路由共用这一份进程内计数(--workers 1 约束)
|
|
limiter = ConnectionLimiter()
|
|
|
|
|
|
def _reset_for_test() -> None:
|
|
limiter._counts.clear()
|