2026-07-07 19:04:13 +08:00
|
|
|
|
"""发送串行化队列。
|
|
|
|
|
|
|
|
|
|
|
|
所有 xdotool 操作经此队列串行执行,避免并发 UI 操作冲突。
|
|
|
|
|
|
内部维护最近 1 秒调用时间戳用于限流。
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
|
|
import asyncio
|
2026-07-10 22:15:45 +08:00
|
|
|
|
import logging
|
2026-07-07 19:04:13 +08:00
|
|
|
|
import time
|
2026-07-16 01:19:47 +08:00
|
|
|
|
from typing import Any, Awaitable, Callable, Optional
|
2026-07-07 19:04:13 +08:00
|
|
|
|
|
2026-07-08 23:25:58 +08:00
|
|
|
|
from woc_bridge.models import BridgeError
|
2026-07-07 19:04:13 +08:00
|
|
|
|
|
|
|
|
|
|
|
2026-07-10 22:15:45 +08:00
|
|
|
|
logger = logging.getLogger("woc-bridge")
|
|
|
|
|
|
|
2026-07-07 19:04:13 +08:00
|
|
|
|
# 工厂类型:返回一个待执行的 coroutine
|
|
|
|
|
|
CoroFactory = Callable[[], Awaitable[Any]]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class SendQueue:
|
|
|
|
|
|
"""串行化发送队列 + 限流。
|
|
|
|
|
|
|
|
|
|
|
|
通过 asyncio.Queue 串行执行所有发送任务;执行间隔可配置
|
2026-07-15 20:14:33 +08:00
|
|
|
|
(默认 3000ms),单实例每秒调用上限可配置(默认 10)。
|
2026-07-07 19:04:13 +08:00
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
def __init__(
|
|
|
|
|
|
self,
|
2026-07-15 20:14:33 +08:00
|
|
|
|
send_delay_ms: int = 3000,
|
2026-07-07 19:04:13 +08:00
|
|
|
|
max_calls_per_sec: int = 10,
|
2026-07-16 01:19:47 +08:00
|
|
|
|
max_queue_size: int = 100,
|
2026-07-07 19:04:13 +08:00
|
|
|
|
) -> None:
|
|
|
|
|
|
"""初始化队列配置。
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
send_delay_ms: 两次发送之间的最小间隔毫秒
|
|
|
|
|
|
max_calls_per_sec: 每秒最大调用次数
|
2026-07-16 01:19:47 +08:00
|
|
|
|
max_queue_size: 队列最大深度,满时入队抛 RATE_LIMITED
|
2026-07-07 19:04:13 +08:00
|
|
|
|
"""
|
|
|
|
|
|
self.send_delay_ms = send_delay_ms
|
|
|
|
|
|
self.max_calls_per_sec = max_calls_per_sec
|
2026-07-16 01:19:47 +08:00
|
|
|
|
self._queue: asyncio.Queue[tuple[CoroFactory, asyncio.Future, Optional[int]]] = asyncio.Queue(maxsize=max_queue_size)
|
2026-07-07 19:04:13 +08:00
|
|
|
|
self._worker: asyncio.Task | None = None
|
|
|
|
|
|
# 最近 1 秒内的调用时间戳
|
|
|
|
|
|
self._recent_call_times: list[float] = []
|
|
|
|
|
|
|
|
|
|
|
|
async def start(self) -> None:
|
|
|
|
|
|
"""启动 worker task。"""
|
|
|
|
|
|
if self._worker is None or self._worker.done():
|
|
|
|
|
|
self._worker = asyncio.create_task(self._run())
|
|
|
|
|
|
|
|
|
|
|
|
async def stop(self) -> None:
|
|
|
|
|
|
"""取消 worker。"""
|
|
|
|
|
|
if self._worker is not None and not self._worker.done():
|
|
|
|
|
|
self._worker.cancel()
|
|
|
|
|
|
try:
|
|
|
|
|
|
await self._worker
|
|
|
|
|
|
except asyncio.CancelledError:
|
|
|
|
|
|
pass
|
|
|
|
|
|
self._worker = None
|
|
|
|
|
|
|
2026-07-17 18:10:31 +08:00
|
|
|
|
async def enqueue(
|
|
|
|
|
|
self,
|
|
|
|
|
|
coro_factory: CoroFactory,
|
|
|
|
|
|
delay_ms: Optional[int] = None,
|
|
|
|
|
|
wait_timeout_ms: Optional[int] = None,
|
|
|
|
|
|
) -> Any:
|
2026-07-07 19:04:13 +08:00
|
|
|
|
"""将一个返回 coroutine 的工厂入队,等待执行结果。
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
coro_factory: 调用后返回 coroutine 的工厂函数
|
2026-07-16 01:19:47 +08:00
|
|
|
|
delay_ms: 自定义延时毫秒,None 用默认 send_delay_ms
|
2026-07-17 18:10:31 +08:00
|
|
|
|
wait_timeout_ms: 队列等待超时毫秒,None 表示无限等待;
|
|
|
|
|
|
超时抛出 BridgeError(TIMEOUT)
|
2026-07-07 19:04:13 +08:00
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
coroutine 的执行结果
|
|
|
|
|
|
|
|
|
|
|
|
Raises:
|
2026-07-17 18:10:31 +08:00
|
|
|
|
BridgeError: 限流命中、队列满或等待超时时抛错;
|
2026-07-07 19:04:13 +08:00
|
|
|
|
任务执行抛出的异常会透传给调用方
|
|
|
|
|
|
"""
|
|
|
|
|
|
loop = asyncio.get_running_loop()
|
|
|
|
|
|
future: asyncio.Future = loop.create_future()
|
2026-07-16 01:19:47 +08:00
|
|
|
|
# 队列满时立即拒绝,避免无界增长 OOM
|
|
|
|
|
|
if self._queue.maxsize > 0 and self._queue.qsize() >= self._queue.maxsize:
|
|
|
|
|
|
logger.warning(
|
|
|
|
|
|
"send_queue: 队列已满 (size=%d/%d),拒绝入队",
|
|
|
|
|
|
self._queue.qsize(), self._queue.maxsize,
|
|
|
|
|
|
)
|
|
|
|
|
|
raise BridgeError(
|
|
|
|
|
|
code="RATE_LIMITED",
|
|
|
|
|
|
message=f"发送队列已满({self._queue.qsize()}/{self._queue.maxsize}),请稍后重试",
|
|
|
|
|
|
details={"retry_after": 3},
|
|
|
|
|
|
)
|
|
|
|
|
|
await self._queue.put((coro_factory, future, delay_ms))
|
2026-07-17 18:10:31 +08:00
|
|
|
|
pending = self._queue.qsize()
|
|
|
|
|
|
logger.info("send_queue: 入队 (pending=%d)", pending)
|
|
|
|
|
|
if wait_timeout_ms is not None and wait_timeout_ms > 0:
|
|
|
|
|
|
try:
|
|
|
|
|
|
return await asyncio.wait_for(
|
|
|
|
|
|
future, timeout=wait_timeout_ms / 1000.0
|
|
|
|
|
|
)
|
|
|
|
|
|
except asyncio.TimeoutError:
|
|
|
|
|
|
# 竞争窗口:worker 可能刚好在此时完成并 set_result。
|
|
|
|
|
|
# 若 future 已完成且未被取消,直接取结果;否则取消 future 并抛 TIMEOUT。
|
|
|
|
|
|
if future.done() and not future.cancelled():
|
|
|
|
|
|
logger.info(
|
|
|
|
|
|
"send_queue: 等待超时但任务刚好完成,取结果 (pending=%d)",
|
|
|
|
|
|
pending,
|
|
|
|
|
|
)
|
|
|
|
|
|
return future.result()
|
|
|
|
|
|
# 取消 future,worker 出队时发现 cancelled 会跳过执行
|
|
|
|
|
|
future.cancel()
|
|
|
|
|
|
wait_sec = wait_timeout_ms / 1000.0
|
|
|
|
|
|
logger.warning(
|
|
|
|
|
|
"send_queue: 等待超时 (pending=%d, wait_timeout=%.1fs)",
|
|
|
|
|
|
pending, wait_sec,
|
|
|
|
|
|
)
|
|
|
|
|
|
raise BridgeError(
|
|
|
|
|
|
code="TIMEOUT",
|
|
|
|
|
|
message=f"发送队列等待超时({pending} 个待处理,已等 {wait_sec:.1f}s)",
|
|
|
|
|
|
details={"retry_after": max(1, int(self.send_delay_ms / 1000))},
|
|
|
|
|
|
)
|
2026-07-07 19:04:13 +08:00
|
|
|
|
return await future
|
|
|
|
|
|
|
|
|
|
|
|
def pending_count(self) -> int:
|
|
|
|
|
|
"""返回当前队列中待执行任务数(供 /api/status 暴露给客户端做退避决策)。"""
|
|
|
|
|
|
return self._queue.qsize()
|
|
|
|
|
|
|
|
|
|
|
|
def _check_rate_limit(self) -> None:
|
|
|
|
|
|
"""检查限流。
|
|
|
|
|
|
|
|
|
|
|
|
清理 1 秒前的时间戳,若当前已满 max_calls_per_sec 则抛
|
|
|
|
|
|
BridgeError(RATE_LIMITED),并在 details 中携带 retry_after 秒数
|
|
|
|
|
|
供上层设置 Retry-After 响应头。
|
|
|
|
|
|
"""
|
|
|
|
|
|
now = time.monotonic()
|
|
|
|
|
|
# 清理 1 秒前的时间戳
|
|
|
|
|
|
self._recent_call_times = [t for t in self._recent_call_times if now - t < 1.0]
|
|
|
|
|
|
if len(self._recent_call_times) >= self.max_calls_per_sec:
|
|
|
|
|
|
# 计算建议等待秒数:最早一次调用距窗口边界还差多久
|
|
|
|
|
|
oldest = self._recent_call_times[0]
|
|
|
|
|
|
retry_after = max(1, int(1.0 - (now - oldest)) + 1)
|
2026-07-10 22:15:45 +08:00
|
|
|
|
logger.warning(
|
|
|
|
|
|
"send_queue: 限流命中,拒绝执行 (recent=%d/%d, retry_after=%ds)",
|
|
|
|
|
|
len(self._recent_call_times), self.max_calls_per_sec, retry_after,
|
|
|
|
|
|
)
|
2026-07-07 19:04:13 +08:00
|
|
|
|
raise BridgeError(
|
|
|
|
|
|
code="RATE_LIMITED",
|
|
|
|
|
|
message=f"发送限流:每秒最多 {self.max_calls_per_sec} 次",
|
|
|
|
|
|
details={"retry_after": retry_after},
|
|
|
|
|
|
)
|
2026-07-17 18:10:31 +08:00
|
|
|
|
logger.debug(
|
|
|
|
|
|
"send_queue: 限流通过 (recent=%d/%d)",
|
|
|
|
|
|
len(self._recent_call_times), self.max_calls_per_sec,
|
|
|
|
|
|
)
|
2026-07-07 19:04:13 +08:00
|
|
|
|
|
|
|
|
|
|
async def _run(self) -> None:
|
|
|
|
|
|
"""worker 主循环。
|
|
|
|
|
|
|
|
|
|
|
|
循环取出任务执行;执行前检查限流(超限则失败该任务);
|
|
|
|
|
|
执行前记录开始时间戳(避免长任务导致 1 秒窗口内超限);
|
2026-07-16 01:19:47 +08:00
|
|
|
|
执行后 sleep delay_ms/1000(自定义延时优先于默认 send_delay_ms)。
|
2026-07-07 19:04:13 +08:00
|
|
|
|
"""
|
|
|
|
|
|
while True:
|
2026-07-16 01:19:47 +08:00
|
|
|
|
coro_factory, future, custom_delay_ms = await self._queue.get()
|
2026-07-17 18:10:31 +08:00
|
|
|
|
# 调用方已超时取消:跳过执行与延时
|
|
|
|
|
|
if future.cancelled():
|
|
|
|
|
|
self._queue.task_done()
|
|
|
|
|
|
logger.info(
|
|
|
|
|
|
"send_queue: 出队任务已取消,跳过 (pending=%d)",
|
|
|
|
|
|
self._queue.qsize(),
|
|
|
|
|
|
)
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
2026-07-10 22:15:45 +08:00
|
|
|
|
logger.info("send_queue: 出队,开始处理 (pending=%d)", self._queue.qsize())
|
2026-07-07 19:04:13 +08:00
|
|
|
|
# 标记任务是否真正开始执行(用于决定 finally 是否延时)
|
|
|
|
|
|
executed = False
|
2026-07-10 22:15:45 +08:00
|
|
|
|
t_exec = time.perf_counter()
|
2026-07-07 19:04:13 +08:00
|
|
|
|
try:
|
|
|
|
|
|
# 执行前检查限流
|
|
|
|
|
|
self._check_rate_limit()
|
|
|
|
|
|
# 记录开始时间戳(限流窗口基于开始时刻,避免长任务后窗口偏移)
|
|
|
|
|
|
self._recent_call_times.append(time.monotonic())
|
|
|
|
|
|
executed = True
|
|
|
|
|
|
# 执行任务
|
2026-07-10 22:15:45 +08:00
|
|
|
|
logger.info("send_queue: 开始执行任务")
|
2026-07-07 19:04:13 +08:00
|
|
|
|
result = await coro_factory()
|
2026-07-10 22:15:45 +08:00
|
|
|
|
logger.info(
|
|
|
|
|
|
"send_queue: 任务执行完成 (%.0fms)",
|
|
|
|
|
|
(time.perf_counter() - t_exec) * 1000,
|
|
|
|
|
|
)
|
2026-07-07 19:04:13 +08:00
|
|
|
|
if not future.done():
|
|
|
|
|
|
future.set_result(result)
|
|
|
|
|
|
except asyncio.CancelledError:
|
|
|
|
|
|
# worker 被取消时,把取消传播给等待的调用方
|
|
|
|
|
|
if not future.done():
|
|
|
|
|
|
future.cancel()
|
|
|
|
|
|
raise
|
|
|
|
|
|
except Exception as e:
|
2026-07-10 22:15:45 +08:00
|
|
|
|
logger.warning(
|
|
|
|
|
|
"send_queue: 任务执行抛异常 %s: %s (%.0fms)",
|
|
|
|
|
|
type(e).__name__, e, (time.perf_counter() - t_exec) * 1000,
|
|
|
|
|
|
)
|
2026-07-07 19:04:13 +08:00
|
|
|
|
if not future.done():
|
|
|
|
|
|
future.set_exception(e)
|
|
|
|
|
|
finally:
|
|
|
|
|
|
self._queue.task_done()
|
2026-07-17 18:10:31 +08:00
|
|
|
|
# 仅在任务真正执行过时延时,限流失败/已取消的任务不延时
|
2026-07-07 19:04:13 +08:00
|
|
|
|
if executed:
|
2026-07-16 01:19:47 +08:00
|
|
|
|
delay = custom_delay_ms if custom_delay_ms is not None else self.send_delay_ms
|
2026-07-10 22:15:45 +08:00
|
|
|
|
logger.info(
|
|
|
|
|
|
"send_queue: 延时 %dms 后处理下一个",
|
2026-07-16 01:19:47 +08:00
|
|
|
|
delay,
|
2026-07-10 22:15:45 +08:00
|
|
|
|
)
|
2026-07-16 01:19:47 +08:00
|
|
|
|
await asyncio.sleep(delay / 1000.0)
|