mirror of
https://github.com/tiennm99/DocsGPT.git
synced 2026-10-03 11:11:58 +00:00
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
63 lines
2.3 KiB
Python
63 lines
2.3 KiB
Python
"""ASGI entrypoint: Flask (WSGI) + FastMCP on the same process."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from a2wsgi import WSGIMiddleware
|
|
from starlette.applications import Starlette
|
|
from starlette.middleware import Middleware
|
|
from starlette.middleware.cors import CORSMiddleware
|
|
from starlette.routing import Mount
|
|
|
|
from docsgpt.api.async_sse import async_sse_routes
|
|
from docsgpt.api.devices.session_events import device_session_routes
|
|
from docsgpt.api.events.routes import event_stream_routes
|
|
from docsgpt.api.user.artifacts.download import artifact_download_routes
|
|
from docsgpt.app import app as flask_app
|
|
from docsgpt.core.settings import settings
|
|
from docsgpt.mcp_server import mcp
|
|
from docsgpt.ui import StaticUI
|
|
|
|
_WSGI_THREADPOOL = int(settings.WSGI_THREADPOOL_WORKERS)
|
|
|
|
mcp_app = mcp.http_app(path="/")
|
|
|
|
# The web UI, when the package ships one (docsgpt/static) and SERVE_UI is on:
|
|
# files are served directly, Flask's own path prefixes pass through, and any
|
|
# other GET renders index.html for the client-side router.
|
|
_backend = StaticUI.wrap(
|
|
WSGIMiddleware(flask_app, workers=_WSGI_THREADPOOL), flask_app.url_map, enabled=settings.SERVE_UI
|
|
)
|
|
|
|
asgi_app = Starlette(
|
|
routes=[
|
|
Mount("/mcp", app=mcp_app),
|
|
# Native-async routes intercept their exact paths before the Flask
|
|
# catch-all. Each holds its response open for a long time (the chat
|
|
# reconnect tail, the notification stream, a device's command stream,
|
|
# large artifact downloads), so it rides the event loop instead of
|
|
# pinning a WSGI threadpool slot. Order matters: Starlette matches
|
|
# routes top-to-bottom, so these must precede the Mount("/") that
|
|
# hands everything else to Flask.
|
|
*async_sse_routes,
|
|
*event_stream_routes,
|
|
*device_session_routes,
|
|
*artifact_download_routes,
|
|
Mount("/", app=_backend),
|
|
],
|
|
middleware=[
|
|
Middleware(
|
|
CORSMiddleware,
|
|
allow_origins=["*"],
|
|
allow_methods=["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
|
|
allow_headers=[
|
|
"Content-Type",
|
|
"Authorization",
|
|
"Mcp-Session-Id",
|
|
"Idempotency-Key",
|
|
],
|
|
expose_headers=["Mcp-Session-Id"],
|
|
),
|
|
],
|
|
lifespan=mcp_app.lifespan,
|
|
)
|