mirror of
https://github.com/Significant-Gravitas/AutoModerate.git
synced 2026-11-08 17:50:32 -10:00
Cloud Run was 500-ing every async route on the eventlet worker:
RuntimeError: asyncio.run() cannot be called from a running event loop
RuntimeError: You cannot use AsyncToSync in the same thread as an
async event loop - just await the async function directly.
Root cause: standard Flask decorators (@login_required from Flask-Login,
@limiter.limit from Flask-Limiter, and Flask's own async-view wrapper)
call ``flask.current_app.ensure_sync(obj)(*a, **k)``, which uses
``asgiref.AsyncToSync``, which calls ``asyncio.run()``. Under eventlet's
greenlet hub ``asyncio.run()`` raises because the hub *is* the running
loop. Every ``async def`` route wrapped by any of those decorators (i.e.
all of /api/moderate, /auth/*, /dashboard/*) dies at the decorator
boundary before the view body ever executes. Sentry AUTOMOD-3B/3C/3D.
The dev server didn't show this because werkzeug gives each request a
fresh OS thread with no existing event loop, so asgiref can spin up its
own loop fine.
gthread worker + SOCKETIO_ASYNC_MODE=threading:
- Real OS threads for each request → asgiref's AsyncToSync works.
- Flask-SocketIO threading mode supports WebSockets for our
dashboard-scale realtime traffic.
- Dev (run.py) and prod now use the same async mode, so behaviour
matches end-to-end.
- 50 threads/worker = plenty of concurrency for HTTP + long-lived
WebSocket connections.
Dropped eventlet and psycogreen from requirements.txt — they were pinned
specifically for the eventlet worker, neither is used any more.
35 lines
1.3 KiB
Python
35 lines
1.3 KiB
Python
"""Production WSGI entry point.
|
|
|
|
Served by gunicorn with the gthread worker class. Example:
|
|
|
|
gunicorn --worker-class gthread --workers 1 --threads 50 \
|
|
--bind 0.0.0.0:6217 wsgi:app
|
|
|
|
Why not eventlet
|
|
----------------
|
|
We previously used eventlet for WebSocket support but it's incompatible
|
|
with Flask's ``async def`` routes under standard decorators
|
|
(``flask_login.login_required``, ``flask_limiter.limit``, Flask's own
|
|
``@app.route`` on async views). Those decorators call
|
|
``flask.current_app.ensure_sync(...)`` which uses ``asgiref.AsyncToSync``
|
|
to invoke the async view, which in turn calls ``asyncio.run()``. Under
|
|
eventlet's greenlet hub ``asyncio.run()`` raises
|
|
"asyncio.run() cannot be called from a running event loop" — so every
|
|
authenticated or rate-limited route 500s in production.
|
|
|
|
gthread + ``SOCKETIO_ASYNC_MODE=threading`` gives us:
|
|
|
|
- Each request handled by a real OS thread, so asgiref can create a loop.
|
|
- Flask-SocketIO in threading mode handles WebSockets via those same
|
|
threads; perfectly fine for our dashboard-scale realtime traffic.
|
|
- Identical behaviour in dev (``run.py``) and prod, so there's one less
|
|
axis along which "works on my machine" can happen.
|
|
"""
|
|
import os
|
|
|
|
os.environ.setdefault('SOCKETIO_ASYNC_MODE', 'threading')
|
|
|
|
from app import create_app # noqa: E402
|
|
|
|
app = create_app(os.getenv('FLASK_CONFIG', 'production'))
|