ForcePilot/backend/package/yuxi/channel/gateway/polling.py

103 lines
3.4 KiB
Python
Raw Normal View History

"""HTTP Polling 降级机制 — SSE/WS 不可用时的备选路径。
session_id 维护响应队列前端通过 HTTP GET 定时轮询获取回复
SSE 端点协同使用形成三级降级链SSE Polling 单次 fetch
Usage:
polling = PollingFallback()
await polling.push("session_abc", {"type": "delta", "data": {"content": "hello"}})
events = await polling.poll("session_abc")
"""
from __future__ import annotations
import asyncio
import logging
import time as _time
logger = logging.getLogger(__name__)
DEFAULT_POLL_TTL = 600
class PollingFallback:
def __init__(self, max_queue_size: int = 100, ttl_seconds: int = DEFAULT_POLL_TTL):
self._queues: dict[str, asyncio.Queue[dict]] = {}
self._ttl = ttl_seconds
self._last_active: dict[str, float] = {}
self._max_queue_size = max_queue_size
self._lock = asyncio.Lock()
async def ensure(self, session_id: str) -> None:
async with self._lock:
if session_id not in self._queues:
self._queues[session_id] = asyncio.Queue(maxsize=self._max_queue_size)
self._last_active[session_id] = _time.monotonic()
async def push(self, session_id: str, event: dict) -> None:
async with self._lock:
self._last_active[session_id] = _time.monotonic()
q = self._queues.get(session_id)
if q is None:
q = asyncio.Queue(maxsize=self._max_queue_size)
self._queues[session_id] = q
try:
q.put_nowait(event)
except asyncio.QueueFull:
logger.warning("Polling queue full for session %s, dropping event", session_id)
async def poll(self, session_id: str) -> list[dict]:
async with self._lock:
self._last_active[session_id] = _time.monotonic()
q = self._queues.get(session_id)
if q is None:
return []
events: list[dict] = []
while not q.empty():
try:
events.append(q.get_nowait())
except asyncio.QueueEmpty:
break
return events
async def cleanup_stale(self) -> int:
async with self._lock:
now = _time.monotonic()
stale = [sid for sid, ts in self._last_active.items() if now - ts > self._ttl]
for sid in stale:
self._queues.pop(sid, None)
self._last_active.pop(sid, None)
if stale:
logger.info("PollingFallback cleaned up %d stale sessions", len(stale))
return len(stale)
def active_sessions(self) -> int:
return len(self._queues)
polling_fallback = PollingFallback()
async def _polling_cleanup_loop(interval: int = 300) -> None:
while True:
await asyncio.sleep(interval)
await polling_fallback.cleanup_stale()
_cleanup_task: asyncio.Task | None = None
def start_polling_cleanup(interval: int = 300) -> None:
global _cleanup_task
if _cleanup_task is None or _cleanup_task.done():
_cleanup_task = asyncio.ensure_future(_polling_cleanup_loop(interval))
logger.info("PollingFallback cleanup loop started (interval=%ds)", interval)
def stop_polling_cleanup() -> None:
global _cleanup_task
if _cleanup_task and not _cleanup_task.done():
_cleanup_task.cancel()
_cleanup_task = None