Hi all,
One of the roadmap threads, alongside Path to Django 6 and Path to Vite: what it takes to serve Ascender from one ASGI server instead of four processes. @cigamit said in the roadmap thread that this has been high on his list for a while, and Django 6 landing was the precondition, so this is the written version of the sequencing.
Where this stands. All four steps have landed. uvicorn serves the API and the websockets, uwsgi and daphne are gone from the supervisor tree, and the only open question is the last one, whether nginx still earns its place, which has its own thread at Path off nginx.
What ran before
The web pod was a supervisor tree, and supervisor_web.conf.j2 listed what was in it:
nginx -> TLS, static files, and routing
uwsgi -> awx.wsgi, 5 processes on 127.0.0.1:8050
daphne -> awx.asgi, websockets on 127.0.0.1:8051
Beside them ran ws-heartbeat, awx-cache-clear and, in development, awx-autoreload. Every HTTP request crossed nginx into uwsgi, every websocket crossed nginx into daphne, and supervisor existed to keep the set alive.
What ASGI changes
ASGI is the async successor to WSGI, and it speaks both HTTP and websockets, which is the whole reason there are two application servers here rather than one. Django has supported it since 3.0 and awx/asgi.py already exists, since daphne needs it. One server means one process to configure, one place where timeouts live, and websockets and HTTP no longer diverging in how they are served.
The candidates, and the one chosen
Three servers could have taken the HTTP traffic. uvicorn was chosen, and it now serves both the API and the websockets:
- daphne: already here and already running the ASGI application, so handing it the HTTP is the smallest step.
- uvicorn: chosen. Where most Django ASGI deployments ended up, faster under HTTP load (#975).
- granian: speaks WSGI and ASGI both, which allows a staged move, and has the least Django mileage.
Either way the application does not change, only what serves it, so the choice wants numbers rather than taste.
What has to keep working
Six things are wired to uwsgi or to the shape of the current tree, and each has to survive the move:
- The statement timeout: given a source that is not uwsgi, so a query is still cancelled (#942).
ATOMIC_REQUESTS: still on, so the views stay sync. It is the reason none were converted (#964).
- The middleware stack: the last sync-only entry is async capable now, so the thread pool is smaller (#964).
- Websockets: unchanged. The routing is in
ascender/asgi.py, and uvicorn serves it beside the HTTP.
- nginx: still there, terminating TLS and serving static files. Whether it stays is the open question.
- Settings reads: three of them are database queries, which the loop thread may not make (#1033).
The sixth was not on the list because nothing suggested it would move. ascender/conf/settings.py answers __dir__, is_overridden and _preload_cache with a database query, which was always a synchronous context under uwsgi and is the event loop under uvicorn, where Django refuses the query rather than block. Nothing in production reads a setting from that thread, so it stayed hidden: what found it was the development debug toolbar, which reads dir(settings) on every response and turned every authenticated request into a 500, days after the move had landed.
The sequence
Four steps, each reviewable on its own, with nothing removed before its replacement is proven:
- Gave the statement timeout a source that does not depend on uwsgi (#942).
- Served HTTP from the ASGI application behind nginx, on uvicorn (#975).
- Retired uwsgi and daphne both, leaving nginx, uvicorn and the supervisor tree (#975).
- Whether nginx and supervisor still earn their place, which is now its own thread.
Two more landed with it, both surfaced by the move: the job stdout download streams instead of buffering into a web worker (#972), and the security headers moved off nginx into the application (#935), one fewer reason to keep it.
Pull requests