54 lines
1.2 KiB
Python
54 lines
1.2 KiB
Python
"""
|
|
固定窗口限流骨架。
|
|
|
|
本文件职责:按 scope 计数;超限返回可重试,不写主账失败态。
|
|
线程:可在出站/API 路径调用;禁止在限流键上做六态。
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from dataclasses import dataclass
|
|
|
|
from agent.redis_coord.client import RedisClient
|
|
from agent.redis_coord.keys import RATE_LIMIT
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
@dataclass
|
|
class RateLimitResult:
|
|
"""限流判定结果。"""
|
|
|
|
allowed: bool
|
|
count: int
|
|
limit: int
|
|
scope: str
|
|
|
|
|
|
def allow(
|
|
client: RedisClient,
|
|
*,
|
|
scope: str,
|
|
limit: int,
|
|
window_seconds: int = 60,
|
|
) -> RateLimitResult:
|
|
"""
|
|
固定窗口:INCR + 首次 EXPIRE。
|
|
|
|
scope 示例:outbound:wecom / api:global / user:{sender_id}
|
|
"""
|
|
key = client.key(RATE_LIMIT, scope)
|
|
count = int(client.raw.incr(key))
|
|
if count == 1:
|
|
client.raw.expire(key, window_seconds)
|
|
allowed = count <= limit
|
|
if not allowed:
|
|
logger.info(
|
|
"限流触发 scope=%s count=%s limit=%s",
|
|
scope,
|
|
count,
|
|
limit,
|
|
)
|
|
return RateLimitResult(allowed=allowed, count=count, limit=limit, scope=scope)
|