2026-02-27 22:22:23 +08:00
|
|
|
|
"""API 速率限制器实现。
|
2026-01-31 03:04:51 +08:00
|
|
|
|
|
2026-02-27 22:22:23 +08:00
|
|
|
|
提供基于固定时间窗口的速率限制,适合 Tushare 等按分钟计费的 API。
|
2026-01-31 03:04:51 +08:00
|
|
|
|
"""
|
2026-02-01 02:29:54 +08:00
|
|
|
|
|
2026-01-31 03:04:51 +08:00
|
|
|
|
import time
|
|
|
|
|
|
import threading
|
|
|
|
|
|
from typing import Optional
|
2026-02-27 22:22:23 +08:00
|
|
|
|
from dataclasses import dataclass
|
2026-01-31 03:04:51 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass
|
|
|
|
|
|
class RateLimiterStats:
|
2026-02-27 22:22:23 +08:00
|
|
|
|
"""速率限制器统计信息。"""
|
2026-02-01 02:29:54 +08:00
|
|
|
|
|
2026-01-31 03:04:51 +08:00
|
|
|
|
total_requests: int = 0
|
|
|
|
|
|
successful_requests: int = 0
|
|
|
|
|
|
denied_requests: int = 0
|
|
|
|
|
|
total_wait_time: float = 0.0
|
2026-02-27 22:22:23 +08:00
|
|
|
|
current_window_requests: int = 0
|
|
|
|
|
|
window_start_time: float = 0.0
|
2026-01-31 03:04:51 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TokenBucketRateLimiter:
|
2026-02-27 22:22:23 +08:00
|
|
|
|
"""基于固定时间窗口的速率限制器。
|
2026-01-31 03:04:51 +08:00
|
|
|
|
|
2026-02-27 22:22:23 +08:00
|
|
|
|
适合 Tushare 等按时间窗口(如每分钟)限制请求数的 API 场景。
|
|
|
|
|
|
在窗口期内,请求数达到上限后将阻塞或等待下一个窗口。
|
2026-01-31 03:04:51 +08:00
|
|
|
|
|
|
|
|
|
|
Attributes:
|
2026-02-27 22:22:23 +08:00
|
|
|
|
capacity: 每个时间窗口内允许的最大请求数
|
|
|
|
|
|
window_seconds: 时间窗口长度(秒)
|
2026-01-31 03:04:51 +08:00
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
def __init__(
|
|
|
|
|
|
self,
|
|
|
|
|
|
capacity: int = 100,
|
|
|
|
|
|
refill_rate_per_second: float = 1.67,
|
|
|
|
|
|
initial_tokens: Optional[int] = None,
|
|
|
|
|
|
) -> None:
|
2026-02-27 22:22:23 +08:00
|
|
|
|
"""初始化速率限制器。
|
2026-01-31 03:04:51 +08:00
|
|
|
|
|
|
|
|
|
|
Args:
|
2026-02-27 22:22:23 +08:00
|
|
|
|
capacity: 每个时间窗口内允许的最大请求数
|
|
|
|
|
|
refill_rate_per_second: 保留参数(向后兼容),实际使用 window_seconds=60
|
|
|
|
|
|
initial_tokens: 保留参数(向后兼容)
|
2026-01-31 03:04:51 +08:00
|
|
|
|
"""
|
|
|
|
|
|
self.capacity = capacity
|
2026-02-27 22:22:23 +08:00
|
|
|
|
# Tushare 通常按分钟限制,所以固定使用 60 秒窗口
|
|
|
|
|
|
self.window_seconds = 60.0
|
|
|
|
|
|
|
|
|
|
|
|
self._requests_in_window = 0
|
|
|
|
|
|
self._window_start = time.monotonic()
|
2026-01-31 03:04:51 +08:00
|
|
|
|
self._lock = threading.RLock()
|
|
|
|
|
|
self._stats = RateLimiterStats()
|
2026-02-27 22:22:23 +08:00
|
|
|
|
self._stats.window_start_time = self._window_start
|
|
|
|
|
|
|
|
|
|
|
|
def _is_new_window(self) -> bool:
|
|
|
|
|
|
"""检查是否已进入新的时间窗口。"""
|
|
|
|
|
|
current_time = time.monotonic()
|
|
|
|
|
|
elapsed = current_time - self._window_start
|
|
|
|
|
|
return elapsed >= self.window_seconds
|
|
|
|
|
|
|
|
|
|
|
|
def _reset_window(self) -> None:
|
|
|
|
|
|
"""重置时间窗口。"""
|
|
|
|
|
|
self._window_start = time.monotonic()
|
|
|
|
|
|
self._requests_in_window = 0
|
|
|
|
|
|
self._stats.window_start_time = self._window_start
|
2026-01-31 03:04:51 +08:00
|
|
|
|
|
2026-02-01 02:29:54 +08:00
|
|
|
|
def acquire(self, timeout: float = float("inf")) -> tuple[bool, float]:
|
2026-02-27 22:22:23 +08:00
|
|
|
|
"""获取请求许可。
|
2026-01-31 03:04:51 +08:00
|
|
|
|
|
2026-02-27 22:22:23 +08:00
|
|
|
|
如果在当前窗口内请求数已达上限,则等待到下一个窗口。
|
2026-01-31 03:04:51 +08:00
|
|
|
|
|
|
|
|
|
|
Args:
|
2026-02-27 22:22:23 +08:00
|
|
|
|
timeout: 最大等待时间(秒),默认无限等待
|
2026-01-31 03:04:51 +08:00
|
|
|
|
|
|
|
|
|
|
Returns:
|
2026-02-27 22:22:23 +08:00
|
|
|
|
(success, wait_time): 是否成功获取许可,以及等待时间
|
2026-01-31 03:04:51 +08:00
|
|
|
|
"""
|
|
|
|
|
|
start_time = time.monotonic()
|
|
|
|
|
|
|
|
|
|
|
|
with self._lock:
|
2026-02-27 22:22:23 +08:00
|
|
|
|
# 检查是否需要进入新窗口
|
|
|
|
|
|
if self._is_new_window():
|
|
|
|
|
|
self._reset_window()
|
2026-01-31 03:04:51 +08:00
|
|
|
|
|
2026-02-27 22:22:23 +08:00
|
|
|
|
# 如果当前窗口还有余量,直接通过
|
|
|
|
|
|
if self._requests_in_window < self.capacity:
|
|
|
|
|
|
self._requests_in_window += 1
|
2026-01-31 03:04:51 +08:00
|
|
|
|
self._stats.total_requests += 1
|
|
|
|
|
|
self._stats.successful_requests += 1
|
2026-02-27 22:22:23 +08:00
|
|
|
|
self._stats.current_window_requests = self._requests_in_window
|
2026-01-31 03:04:51 +08:00
|
|
|
|
return True, 0.0
|
|
|
|
|
|
|
2026-02-27 22:22:23 +08:00
|
|
|
|
# 当前窗口已满,计算需要等待的时间
|
|
|
|
|
|
current_time = time.monotonic()
|
|
|
|
|
|
time_to_next_window = self.window_seconds - (
|
|
|
|
|
|
current_time - self._window_start
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if time_to_next_window <= 0:
|
|
|
|
|
|
# 刚好进入新窗口
|
|
|
|
|
|
self._reset_window()
|
|
|
|
|
|
self._requests_in_window = 1
|
|
|
|
|
|
self._stats.total_requests += 1
|
|
|
|
|
|
self._stats.successful_requests += 1
|
|
|
|
|
|
self._stats.current_window_requests = 1
|
|
|
|
|
|
return True, 0.0
|
2026-01-31 03:04:51 +08:00
|
|
|
|
|
2026-02-27 22:22:23 +08:00
|
|
|
|
# 检查是否能在超时时间内等待
|
|
|
|
|
|
if timeout != float("inf") and time_to_next_window > timeout:
|
2026-01-31 03:04:51 +08:00
|
|
|
|
self._stats.total_requests += 1
|
|
|
|
|
|
self._stats.denied_requests += 1
|
|
|
|
|
|
return False, timeout
|
|
|
|
|
|
|
2026-02-27 22:22:23 +08:00
|
|
|
|
# 需要等待到下一个窗口
|
|
|
|
|
|
if timeout != float("inf"):
|
|
|
|
|
|
time_to_wait = min(time_to_next_window, timeout)
|
|
|
|
|
|
else:
|
|
|
|
|
|
time_to_wait = time_to_next_window
|
|
|
|
|
|
|
|
|
|
|
|
time.sleep(time_to_wait)
|
|
|
|
|
|
|
|
|
|
|
|
# 重新尝试获取许可
|
|
|
|
|
|
with self._lock:
|
|
|
|
|
|
# 再次检查窗口状态(可能其他线程已经重置了窗口)
|
|
|
|
|
|
if self._is_new_window():
|
|
|
|
|
|
self._reset_window()
|
|
|
|
|
|
|
|
|
|
|
|
if self._requests_in_window < self.capacity:
|
|
|
|
|
|
self._requests_in_window += 1
|
|
|
|
|
|
wait_time = time.monotonic() - start_time
|
|
|
|
|
|
self._stats.total_requests += 1
|
|
|
|
|
|
self._stats.successful_requests += 1
|
|
|
|
|
|
self._stats.total_wait_time += wait_time
|
|
|
|
|
|
self._stats.current_window_requests = self._requests_in_window
|
|
|
|
|
|
return True, wait_time
|
|
|
|
|
|
else:
|
|
|
|
|
|
# 在极端情况下,等待后仍然无法获取(其他线程抢先)
|
|
|
|
|
|
wait_time = time.monotonic() - start_time
|
|
|
|
|
|
self._stats.total_requests += 1
|
|
|
|
|
|
self._stats.denied_requests += 1
|
|
|
|
|
|
return False, wait_time
|
2026-01-31 03:04:51 +08:00
|
|
|
|
|
|
|
|
|
|
def acquire_nonblocking(self) -> tuple[bool, float]:
|
2026-02-27 22:22:23 +08:00
|
|
|
|
"""尝试非阻塞地获取请求许可。
|
2026-01-31 03:04:51 +08:00
|
|
|
|
|
|
|
|
|
|
Returns:
|
2026-02-27 22:22:23 +08:00
|
|
|
|
(success, wait_time): 是否成功获取许可,以及需要等待的时间
|
2026-01-31 03:04:51 +08:00
|
|
|
|
"""
|
|
|
|
|
|
with self._lock:
|
2026-02-27 22:22:23 +08:00
|
|
|
|
# 检查是否需要进入新窗口
|
|
|
|
|
|
if self._is_new_window():
|
|
|
|
|
|
self._reset_window()
|
2026-01-31 03:04:51 +08:00
|
|
|
|
|
2026-02-27 22:22:23 +08:00
|
|
|
|
# 如果当前窗口还有余量,直接通过
|
|
|
|
|
|
if self._requests_in_window < self.capacity:
|
|
|
|
|
|
self._requests_in_window += 1
|
2026-01-31 03:04:51 +08:00
|
|
|
|
self._stats.total_requests += 1
|
|
|
|
|
|
self._stats.successful_requests += 1
|
2026-02-27 22:22:23 +08:00
|
|
|
|
self._stats.current_window_requests = self._requests_in_window
|
2026-01-31 03:04:51 +08:00
|
|
|
|
return True, 0.0
|
|
|
|
|
|
|
2026-02-27 22:22:23 +08:00
|
|
|
|
# 当前窗口已满,计算需要等待的时间
|
|
|
|
|
|
current_time = time.monotonic()
|
|
|
|
|
|
time_to_next_window = self.window_seconds - (
|
|
|
|
|
|
current_time - self._window_start
|
|
|
|
|
|
)
|
2026-01-31 03:04:51 +08:00
|
|
|
|
|
|
|
|
|
|
self._stats.total_requests += 1
|
|
|
|
|
|
self._stats.denied_requests += 1
|
2026-02-27 22:22:23 +08:00
|
|
|
|
return False, max(0.0, time_to_next_window)
|
2026-01-31 03:04:51 +08:00
|
|
|
|
|
|
|
|
|
|
def get_available_tokens(self) -> float:
|
2026-02-27 22:22:23 +08:00
|
|
|
|
"""获取当前窗口剩余可用请求数。
|
2026-01-31 03:04:51 +08:00
|
|
|
|
|
|
|
|
|
|
Returns:
|
2026-02-27 22:22:23 +08:00
|
|
|
|
当前窗口剩余可用请求数
|
2026-01-31 03:04:51 +08:00
|
|
|
|
"""
|
|
|
|
|
|
with self._lock:
|
2026-02-27 22:22:23 +08:00
|
|
|
|
if self._is_new_window():
|
|
|
|
|
|
return float(self.capacity)
|
|
|
|
|
|
return float(self.capacity - self._requests_in_window)
|
2026-01-31 03:04:51 +08:00
|
|
|
|
|
|
|
|
|
|
def get_stats(self) -> RateLimiterStats:
|
2026-02-27 22:22:23 +08:00
|
|
|
|
"""获取速率限制器统计信息。
|
2026-01-31 03:04:51 +08:00
|
|
|
|
|
|
|
|
|
|
Returns:
|
2026-02-27 22:22:23 +08:00
|
|
|
|
RateLimiterStats 实例
|
2026-01-31 03:04:51 +08:00
|
|
|
|
"""
|
|
|
|
|
|
with self._lock:
|
2026-02-27 22:22:23 +08:00
|
|
|
|
self._stats.current_window_requests = self._requests_in_window
|
2026-01-31 03:04:51 +08:00
|
|
|
|
return self._stats
|