Files
group_fqcd_jr/app/core/rate_limit.py
T

52 lines
2.1 KiB
Python
Raw Normal View History

"""限流策略值与错误载具(文档 §3.5 的 429、§3.6 的 `RATE_LIMITED`)。
放在 `app/core` 而不是 `errors.py`:码值与状态码的唯一口径在 `app/core/errors.py`,
这里只做两件事——把配置读成不可变策略对象,以及给 `429` 挂上文档要求的
`Retry-After` 数值载体(`RateLimitedError` 本身不携带秒数)。
"""
from dataclasses import dataclass
from app.core.config import Settings
from app.core.errors import RateLimitedError
class RateLimitExceededError(RateLimitedError):
"""`429 RATE_LIMITED` 的运行时载具:额外携带 `Retry-After` 秒数。
`code` / `status_code` / `retryable` 全部继承 `RateLimitedError`(文档 §3.6 口径:
429、可重试),`app/main.py` 的处理器据此输出统一错误信封并附加 `Retry-After` 头。
"""
def __init__(self, message: str, retry_after_seconds: int) -> None:
super().__init__(message)
# 头部取值必须是正整数秒:0 会让"立刻重试"变成忙等,负数非法。
self.retry_after_seconds = max(1, int(retry_after_seconds))
@dataclass(frozen=True)
class RateLimitPolicy:
"""限流策略:按"用户 + 方法 + 路由"在固定窗口内计数。
维度取"用户 + 接口"而不是全站总量:单个客户端的异常流量不应该把其他用户一起
拖下水;不同接口的成本差异很大,混在一个计数器里会让慢接口拖累快接口。
"""
enabled: bool
window_seconds: int
max_requests: int
key_prefix: str
@classmethod
def from_settings(cls, settings: Settings) -> "RateLimitPolicy":
return cls(
enabled=settings.rate_limit_enabled,
window_seconds=settings.rate_limit_window_seconds,
max_requests=settings.rate_limit_max_requests,
key_prefix=settings.rate_limit_key_prefix,
)
def key(self, user_id: str, method: str, route_path: str) -> str:
"""计数键。用路由**模板**而不是原始 URL:`/{run_id}` 下每个 run 都是同一接口。"""
return f"{self.key_prefix}:{user_id}:{method.upper()}:{route_path}"