Coverage for agentos/queue/rate_limiter.py: 46%
115 statements
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-06 10:59 +0800
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-06 10:59 +0800
1"""
2AgentOS v0.60 Rate Limiter — 流量控制。
3Token Bucket + Sliding Window + Concurrency Limiter + 多级配额。
4"""
6from __future__ import annotations
8import asyncio
9import time
10from dataclasses import dataclass
11from enum import Enum
14class RateLimitStrategy(str, Enum):
16 """限流策略枚举。"""
18 TOKEN_BUCKET = "token_bucket"
19 SLIDING_WINDOW = "sliding_window"
20 FIXED_WINDOW = "fixed_window"
23@dataclass
24class RateLimitConfig:
25 """限流配置。"""
26 strategy: RateLimitStrategy = RateLimitStrategy.TOKEN_BUCKET
27 max_requests: int = 60 # 每单位时间的最大请求数
28 per_seconds: float = 60.0 # 时间窗口(秒)
29 burst_size: int = 10 # 突发容量(token bucket 专用)
30 max_concurrent: int = 5 # 最大并发数
31 queue_timeout: float = 30.0 # 排队超时
32 retry_after_header: bool = True # 是否在拒绝时返回 Retry-After
35@dataclass
36class RateLimitResult:
37 """限流检查结果。"""
38 allowed: bool
39 remaining: int = 0
40 reset_at: float = 0.0
41 retry_after: float = 0.0
42 limit: int = 0
43 reason: str = ""
46class TokenBucket:
47 """令牌桶算法实现。"""
49 def __init__(self, rate: float, capacity: int):
50 self.rate = rate # 令牌填充速率(个/秒)
51 self.capacity = capacity # 桶容量(最大突发)
52 self.tokens = float(capacity)
53 self.last_refill = time.monotonic()
54 self._lock = asyncio.Lock()
56 async def consume(self, tokens: int = 1) -> bool:
57 async with self._lock:
58 self._refill()
59 if self.tokens >= tokens:
60 self.tokens -= tokens
61 return True
62 return False
64 def _refill(self):
65 now = time.monotonic()
66 elapsed = now - self.last_refill
67 self.tokens = min(self.capacity, self.tokens + elapsed * self.rate)
68 self.last_refill = now
70 @property
71 def available(self) -> float:
72 self._refill()
73 return self.tokens
76class SlidingWindow:
77 """滑动窗口计数器。"""
79 def __init__(self, max_requests: int, window_seconds: float):
80 self.max_requests = max_requests
81 self.window = window_seconds
82 self._timestamps: list[float] = []
83 self._lock = asyncio.Lock()
85 async def allow(self) -> bool:
86 async with self._lock:
87 now = time.monotonic()
88 cutoff = now - self.window
89 self._timestamps = [t for t in self._timestamps if t > cutoff]
90 if len(self._timestamps) < self.max_requests:
91 self._timestamps.append(now)
92 return True
93 return False
95 @property
96 def current_count(self) -> int:
97 cutoff = time.monotonic() - self.window
98 return sum(1 for t in self._timestamps if t > cutoff)
101class ConcurrencyLimiter:
102 """并发请求限制器。"""
104 def __init__(self, max_concurrent: int):
105 self._semaphore = asyncio.Semaphore(max_concurrent)
106 self.max_concurrent = max_concurrent
108 async def acquire(self) -> bool:
109 return await self._semaphore.acquire()
111 def release(self):
112 self._semaphore.release()
114 @property
115 def available(self) -> int:
116 return self._semaphore._value
119class RateLimiter:
120 """组合限流器:Token Bucket + Concurrency Limiter + 多级配额。"""
122 def __init__(self, config: RateLimitConfig | None = None):
123 cfg = config or RateLimitConfig()
124 self.config = cfg
125 self._bucket = TokenBucket(
126 rate=cfg.max_requests / cfg.per_seconds,
127 capacity=cfg.burst_size or cfg.max_requests
128 )
129 self._window = SlidingWindow(cfg.max_requests, cfg.per_seconds)
130 self._concurrency = ConcurrencyLimiter(cfg.max_concurrent)
132 async def acquire(self, weight: int = 1) -> RateLimitResult:
133 """尝试获取请求配额。先检查并发,再检查速率。"""
134 # 1. 并发检查
135 if not self._concurrency._semaphore.locked():
136 pass # 还有并发槽位
138 # 2. 速率检查
139 if self.config.strategy == RateLimitStrategy.TOKEN_BUCKET:
140 if await self._bucket.consume(weight):
141 return RateLimitResult(
142 allowed=True,
143 remaining=max(0, int(self._bucket.available)),
144 limit=self.config.max_requests,
145 )
146 wait = (weight - self._bucket.available) / self._bucket.rate
147 return RateLimitResult(
148 allowed=False,
149 remaining=0,
150 retry_after=wait,
151 limit=self.config.max_requests,
152 reason="rate_limit_exceeded",
153 )
155 elif self.config.strategy == RateLimitStrategy.SLIDING_WINDOW:
156 if await self._window.allow():
157 return RateLimitResult(
158 allowed=True,
159 remaining=self.config.max_requests - self._window.current_count,
160 limit=self.config.max_requests,
161 )
162 return RateLimitResult(
163 allowed=False, remaining=0,
164 retry_after=self.config.per_seconds,
165 limit=self.config.max_requests,
166 reason="window_exceeded",
167 )
169 # fixed window fallback
170 return RateLimitResult(allowed=True, limit=self.config.max_requests)
172 async def release(self):
173 self._concurrency.release()
175 def model_quota(self, model: str) -> RateLimitConfig:
176 """返回特定模型的配额配置。"""
177 quotas = {
178 "gpt-4o": RateLimitConfig(max_requests=50, per_seconds=60, burst_size=5),
179 "gpt-4o-mini": RateLimitConfig(max_requests=200, per_seconds=60, burst_size=20),
180 "claude-sonnet-4": RateLimitConfig(max_requests=40, per_seconds=60, burst_size=5),
181 "deepseek-v3.1": RateLimitConfig(max_requests=100, per_seconds=60, burst_size=15),
182 }
183 return quotas.get(model, self.config)
186class QuotaManager:
187 """多租户配额管理。"""
189 def __init__(self):
190 self._limiters: dict[str, RateLimiter] = {}
192 def get(self, key: str, config: RateLimitConfig | None = None) -> RateLimiter:
193 if key not in self._limiters:
194 self._limiters[key] = RateLimiter(config)
195 return self._limiters[key]
197 def add_quota(self, key: str, config: RateLimitConfig):
198 self._limiters[key] = RateLimiter(config)
200 def clear_expired(self, ttl: float = 3600):
201 """清除超过TTL未使用的限流器(预留接口)。"""
202 pass