ForcePilot/backend/package/yuxi/channels/application/rate_limit_checker.py
Kris b10cb080de refactor: 多模块代码优化与功能增强
本次提交包含多项代码改进与功能增强:
1. 新增服务账号禁用错误类型与状态映射
2. 优化出站管道阶段顺序与上下文字段
3. 完善会话与事务管理逻辑,修复并发冲突处理
4. 调整钉钉与微信插件的适配逻辑
5. 优化内容校验与限流降级策略
6. 新增渠道降级投递事件与围栏回滚机制
7. 修正配置schema与权限校验逻辑
8. 完善审计与监控相关的日志与钩子处理
2026-07-09 13:51:58 +08:00

112 lines
4.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""限流原子计数共享 helper。
封装 ``CachePort.incr + expire`` 原子限流范式,消除 ``get/set`` 非原子
读改写导致的高并发超额放行C-C1。供 ``rate_limit_stage`` 与
``admin_message_service`` 复用,避免重复实现错误范式。
故障语义(与 ``health_check_service._checkProbeRateLimit`` 对齐):
- ``incr`` 失败(``DependencyError``)→ fail-closed请求尚未计数翻译
为 ``RateLimitError`` 拒绝,避免 Redis 故障期间限流失效放行超额请求。
- ``expire`` 失败(``DependencyError``)→ fail-fast请求已计数直接
上抛 ``DependencyError`` 暴露基础设施故障INV-7不在此处吞异常——
窗口未设置会导致计数永久累积,限流失效。
- 首次递增(``count == 1``)设置窗口 TTL对应 Redis ``EXPIRE``,只设
TTL 不修改值,消除"首次 ``set`` 覆盖并发 ``incr`` 计数"的竞态)。
"""
from __future__ import annotations
from yuxi.channels.contract.errors import DependencyError, RateLimitError
from yuxi.channels.contract.ports.driven.cache_port import CachePort
from yuxi.channels.contract.ports.driven.logger_port import LoggerPort
__all__ = ["RateLimitChecker"]
class RateLimitChecker:
"""原子限流计数器,封装 ``incr + expire`` 范式。
单一职责:基于 ``CachePort`` 原子操作校验单 key 计数是否超限。不负责
key 构造与阈值配置(由调用方传入),``resource`` 字段标识限流作用域。
用法::
checker = RateLimitChecker(cache_port, logger)
await checker.check(
resource=f"control-plane:{user_id}:{ip}",
cache_key=f"rate_limit:{user_id}:{ip}",
max_requests=100,
window_seconds=60,
retry_after_ms=60_000,
trace_id=trace_id,
)
"""
def __init__(self, cache_port: CachePort, logger: LoggerPort | None = None) -> None:
"""初始化限流计数器。
参数:
cache_port: 缓存被驱动端口,提供 ``incr`` / ``expire`` 原子操作。
logger: 日志端口(可选),``incr`` 故障 fail-closed 时记录告警。
"""
self._cache = cache_port
self._logger = logger
async def check(
self,
*,
resource: str,
cache_key: str,
max_requests: int,
window_seconds: int,
retry_after_ms: int,
trace_id: str | None = None,
) -> None:
"""校验限流计数是否超限,超限或缓存故障时抛出。
参数:
resource: 限流资源标识(写入 ``RateLimitError.resource``)。
cache_key: 缓存计数 key。
max_requests: 窗口内最大请求数(``count > max_requests`` 拒绝)。
window_seconds: 限流窗口秒数(首次递增时设置 TTL
retry_after_ms: 建议重试等待毫秒。
trace_id: 追踪 ID写入异常上下文与日志
抛出:
RateLimitError: 请求频率超限,或 ``incr`` 缓存故障时 fail-closed。
DependencyError: ``expire`` 缓存故障时 fail-fast请求已计数
"""
try:
count = await self._cache.incr(cache_key)
except DependencyError as exc:
# incr 失败时请求尚未计数fail-closed 翻译为 RateLimitError
# 拒绝(与 health_check_service._checkProbeRateLimit 一致INV-7
# 避免 Redis 故障期间限流失效放行超额请求。
if self._logger is not None:
await self._logger.error(
"rate_limit cache failure, fail-closed reject",
trace_id=trace_id,
cache_key=cache_key,
error=str(exc),
)
raise RateLimitError(
resource=resource,
retry_after_ms=retry_after_ms,
trace_id=trace_id,
) from exc
# 首次递增count == 1设置 TTL 窗口。expire 只设过期时间不修改值,
# 避免 set 覆盖高并发期间其他请求的 incr 计数。
#
# 故障策略与 incr 不同expire 失败时请求已计数fail-fast 直接抛
# DependencyError 暴露基础设施故障INV-7不吞异常——窗口未设置
# 会导致计数永久累积,限流失效。
if count == 1:
await self._cache.expire(cache_key, window_seconds)
if count > max_requests:
raise RateLimitError(
resource=resource,
retry_after_ms=retry_after_ms,
trace_id=trace_id,
)