新增设备身份管理、认证限流、并发通道、Webhook路由、RBAC权限控制、SSE/轮询降级等全套网关通道功能,包含: 1. 设备身份生成与签名验证 2. 设备令牌认证与速率限制 3. 内存+数据库双重设备注册表 4. 并发通道限流管理 5. Webhook安全处理与路由 6. RBAC权限校验系统 7. OpenAI API兼容适配层 8. Tailscale认证支持 9. HTTP轮询降级机制
103 lines
3.4 KiB
Python
103 lines
3.4 KiB
Python
"""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
|