feat(api): serve long-lived streams on the event loop

Move GET /api/events, the remote-device command stream and artifact
downloads from Flask to Starlette routes mounted ahead of the Flask
catch-all. On Flask each held an a2wsgi threadpool slot for as long as
its response stayed open, and because uvicorn drops writes after a
client disconnects, a closed tab never released it.

- asgi_auth: one JWT/OIDC gate for Starlette routes; the chat reconnect
  reader uses it too
- ClosingStreamingResponse closes the body iterator and releases the
  SSE slot or file handle even when the client leaves before the first
  frame
- AsyncTopic liveness probe replaces the sync client's socket_timeout
  guard against half-open pub/sub sockets
- ASYNC_REDIS_MAX_CONNECTIONS sizes the async Redis pool; every open
  stream holds a connection and redis-py defaults to 100
This commit is contained in:
Alex committed 2026-09-14 08:33:46 +01:00
1 parent 38773471a8
commit 54b540ea42
33 files changed
+2822 -1476

No files matched your search

+44
View File
@@ -0,0 +1,44 @@
"""Streaming response for native-async routes whose streams hold resources."""
from __future__ import annotations
import logging
from typing import Awaitable, Callable, Optional
import anyio
from starlette.responses import StreamingResponse
from starlette.types import Receive, Scope, Send
logger = logging.getLogger(__name__)
class ClosingStreamingResponse(StreamingResponse):
"""``StreamingResponse`` that always finalizes its stream.
When a client disconnects, Starlette cancels the send loop and drops the
body iterator: a generator paused between frames, or one that never
started, would leave its cleanup to garbage collection. This closes the
iterator and then runs ``on_close`` once the response is over, shielded
from the cancellation that ended it.
"""
def __init__(self, content, *, on_close: Optional[Callable[[], Awaitable[None]]] = None, **kwargs) -> None:
super().__init__(content, **kwargs)
self._on_close = on_close
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
try:
await super().__call__(scope, receive, send)
finally:
with anyio.CancelScope(shield=True):
aclose = getattr(self.body_iterator, "aclose", None)
if aclose is not None:
try:
await aclose()
except Exception:
logger.exception("closing the response stream failed")
if self._on_close is not None:
try:
await self._on_close()
except Exception:
logger.exception("response on_close hook failed")