本次提交包含多项代码改进与功能增强: 1. 新增服务账号禁用错误类型与状态映射 2. 优化出站管道阶段顺序与上下文字段 3. 完善会话与事务管理逻辑,修复并发冲突处理 4. 调整钉钉与微信插件的适配逻辑 5. 优化内容校验与限流降级策略 6. 新增渠道降级投递事件与围栏回滚机制 7. 修正配置schema与权限校验逻辑 8. 完善审计与监控相关的日志与钩子处理
112 lines
4.7 KiB
Python
112 lines
4.7 KiB
Python
"""限流原子计数共享 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,
|
||
)
|