| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798 |
- """限流缓冲队列:单 Worker 匀速消费,保证 < max_qps QPS。
- 飞书限制:同一群发送消息上限 5 QPS。本模块用 asyncio.Queue 削峰,
- 独立 Worker 协程以 1/max_qps 秒/条的速率匀速消费,避免瞬时并发触发封禁。
- """
- from __future__ import annotations
- import asyncio
- import logging
- from dataclasses import dataclass
- from typing import Awaitable, Callable, Optional
- logger = logging.getLogger(__name__)
- @dataclass
- class ForwardTask:
- """待转发的单条任务:源消息 id -> 单个目标群。"""
- message_id: str
- target_chat_id: str
- # 可选:携带源信息用于日志
- source_chat_id: str = ""
- class RateLimitedWorker:
- """单消费协程 + asyncio.Queue,匀速向目标群转发。
- - 入队即返回(非阻塞),由 Worker 串行消费
- - 每条间隔 1/max_qps 秒,确保 QPS 不超限
- - 单条失败按 max_retry 退避重试
- """
- def __init__(
- self,
- max_qps: int,
- max_retry: int,
- forward_fn: Optional[Callable[[ForwardTask], Awaitable[bool]]] = None,
- queue: Optional[asyncio.Queue] = None,
- ) -> None:
- if max_qps <= 0:
- raise ValueError("max_qps 必须为正数")
- self._interval = 1.0 / max_qps
- self._max_retry = max_retry
- self._forward_fn = forward_fn
- self._queue: asyncio.Queue = queue or asyncio.Queue()
- def set_forward_fn(self, fn: Callable[[ForwardTask], Awaitable[bool]]) -> None:
- """后置注入转发函数(解决与 Forwarder 的循环依赖)。"""
- self._forward_fn = fn
- async def enqueue(self, task: ForwardTask) -> None:
- await self._queue.put(task)
- async def run(self) -> None:
- logger.info("转发 Worker 启动,目标速率 %.2f QPS(间隔 %.3fs)",
- 1.0 / self._interval, self._interval)
- while True:
- task: ForwardTask = await self._queue.get()
- try:
- await self._process(task)
- except asyncio.CancelledError:
- logger.info("Worker 收到取消信号,退出")
- raise
- except Exception as e:
- logger.exception("Worker 处理异常: %s", e)
- finally:
- self._queue.task_done()
- async def _process(self, task: ForwardTask) -> None:
- if self._forward_fn is None:
- logger.error("forward_fn 未设置,丢弃任务 message_id=%s", task.message_id)
- return
- for attempt in range(self._max_retry + 1):
- ok = await self._forward_fn(task)
- if ok:
- if attempt > 0:
- logger.info("重试成功 message_id=%s -> %s(第 %d 次)",
- task.message_id, task.target_chat_id, attempt)
- break
- # 失败:退避后重试
- backoff = self._interval * (attempt + 1) * 2
- logger.warning("转发失败 message_id=%s -> %s,%ds 后重试(%d/%d)",
- task.message_id, task.target_chat_id,
- int(backoff), attempt + 1, self._max_retry)
- await asyncio.sleep(backoff)
- else:
- logger.error("转发最终失败 message_id=%s -> %s,已放弃",
- task.message_id, task.target_chat_id)
- # 成功或失败都遵守速率间隔
- await asyncio.sleep(self._interval)
- async def wait_drained(self, timeout: Optional[float] = None) -> bool:
- """等待队列清空,返回是否在 timeout 内清空。"""
- try:
- await asyncio.wait_for(self._queue.join(), timeout=timeout)
- return True
- except asyncio.TimeoutError:
- return False
|