Django 3.0, uscito il 2 dicembre 2019, si può servire come applicazione ASGI accanto al solito WSGI, ma view, middleware, ORM e cache restano del tutto sincroni. Il cambiamento tocca lo strato più esterno della catena che gestisce la richiesta, non il codice applicativo che si scrive tutti i giorni.
Contesto
ASGI (Asynchronous Server Gateway Interface) è la specifica che estende WSGI (Web Server Gateway Interface) ai protocolli che WSGI non sapeva descrivere: connessioni di lunga durata, WebSocket, server-sent events e in generale qualunque scambio che non si riduca a una singola coppia richiesta/risposta su un thread. La specifica circolava già fuori dal core di Django grazie al progetto Channels, che dal 2016 dava un percorso per WebSocket e task in background; con la 3.0 l’interfaccia entra nel framework.
Il motivo tecnico è il carico I/O-bound. Una view che aspetta la risposta di un servizio esterno a latenza variabile tiene occupato un worker per tutta l’attesa, anche quando il processo non fa niente. Sotto WSGI la concorrenza si ottiene moltiplicando thread o processi, e si paga in memoria e context switch. Il modello a singolo event loop di asyncio, nella libreria standard dalla 3.4 e con la sintassi async/await dalla 3.5, regge molte connessioni in attesa su un thread solo.
Cosa fa davvero Django 3.0
Il progetto generato da startproject ora porta un asgi.py accanto a wsgi.py. Punta a get_asgi_application(), che restituisce un’istanza di ASGIHandler. Questo handler parla il protocollo ASGI verso il server — Daphne o Uvicorn, per dire — e dentro di sé traduce ogni richiesta nel solito ciclo sincrono.
La traduzione passa per asgiref, la libreria di utilità che accompagna la specifica. Al centro ci sono due funzioni:
sync_to_asyncavvolge una callable sincrona perché si possa attendere da codice asincrono, eseguendola in un thread pool.async_to_syncfa l’opposto, esegue una coroutine da un contesto sincrono fino a quando finisce.
ASGIHandler riceve l’evento dal loop in modo asincrono, costruisce l’oggetto HttpRequest, poi passa la risoluzione dell’URL, il middleware e la view allo stesso codice sincrono della 2.x, avvolto in sync_to_async. La risposta torna indietro per la stessa strada. Di per sé questa configurazione guadagna poco: il corpo della view gira comunque su un thread del pool, quindi una view tradizionale servita via ASGI non va più veloce, e a volte ci rimette un piccolo overhead rispetto a WSGI.
Il punto critico
La 3.0 vale per quello che ci si può costruire sopra l’handler ASGI; il codice esistente non gira più in fretta. Con Django sotto un server ASGI, nella stessa applicazione si monta codice che parla il protocollo in modo nativo: routing che distingue gli scope http e websocket, gestori di lunga durata, integrazione con Channels senza un processo a parte. La richiesta HTTP classica continua a passare dal percorso sincrono; i nuovi tipi di connessione, invece, trovano finalmente posto dentro il framework.
C’è un vincolo di compatibilità che conviene dire chiaro. La documentazione di Django 3.0 avverte: se da un contesto asincrono si chiama codice sincrono che a sua volta tocca il database, saltano le garanzie dell’ORM, costruito sull’assunzione di un thread per richiesta. Per questo l’handler isola l’esecuzione sincrona in thread dedicati invece di farla girare sul loop. Chi salta questo confine — per esempio lanciando una query con async_to_sync da una coroutine condivisa — raccoglie errori difficili da diagnosticare. Il confine tra i due mondi si attraversa solo passando da asgiref, mai a mano.
La roadmap dietro la scelta
Niente di improvvisato: l’apertura viene dal DEP 0009 (“Async-capable Django”), dove DEP sta per Django Enhancement Proposal — la proposta di Andrew Godwin approvata dal Technical Board nel luglio 2019. Nella sezione “Sequencing” il documento pianifica un primo round (idealmente la 3.0) che mette insieme gestione HTTP, middleware e view in versione asincrona nativa con wrapper sincrono, e rimanda ai passi successivi i pezzi più profondi — ORM, cache, autenticazione. Il principio dichiarato è introdurre l’asincrono all’inizio facendo girare il codice sincrono nei thread, e sostituirlo con implementazioni native solo dove il beneficio è concreto, senza mai rompere le applicazioni già scritte.
Conviene separare quello che il DEP pianificava da quello che la 3.0 ha poi portato a casa. Il primo round del documento prevedeva già view e middleware asincroni, ma la release si è fermata all’handler ASGI esterno e li ha rimandati alla versione dopo. Nella 3.0, quindi, dichiarare una view con async def non produce alcun comportamento asincrono: il framework la esegue comunque sul percorso sincrono. L’ORM, il pezzo più ostico da convertire perché QuerySet, manager e relazioni poggiano su operazioni bloccanti, resta sincrono ed è collocato nel DEP tra i pezzi profondi, senza una data fissa.
Implicazioni pratiche
Per una codebase Django già esistente, passare alla 3.0 non obbliga ad adottare ASGI. WSGI resta supportato ed è ancora la scelta giusta per un’applicazione che serve solo richieste HTTP tradizionali: tirare dentro un server ASGI per far girare codice del tutto sincrono aggiunge complessità senza un guadagno misurabile.
Spostarsi su ASGI ha senso quando c’è un requisito concreto che WSGI non copre — WebSocket, streaming, integrazione con Channels nello stesso processo — oppure per farsi trovare pronti quando view e middleware asincroni arriveranno nel framework. Conviene controllare che il server scelto sia compatibile con la versione di ASGI che usa Django 3.0 e che eventuali librerie di terze parti sul percorso della richiesta non diano per scontato WSGI.
Limiti
A oggi, aprile 2020, l’asincrono in Django si ferma all’handler esterno: tutto quello che sta dentro la richiesta è ancora sincrono. Le misure di throughput su carichi reali vanno fatte sulla propria applicazione, perché servire codice sincrono via ASGI può venire più lento che via WSGI, per via dell’attraversamento del thread pool. Le parti successive del DEP non hanno una data di rilascio garantita, e una stima a calendario su un progetto open source portato avanti da volontari sarebbe poco affidabile. Chi valuta la migrazione faccia conto su quello che è documentato per la 3.0, non su quello che la roadmap promette per il futuro.
- https://docs.djangoproject.com/en/3.0/releases/3.0/
- https://docs.djangoproject.com/en/3.0/howto/deployment/asgi/
- https://asgi.readthedocs.io/en/latest/
- https://github.com/django/asgiref
- https://github.com/django/deps/blob/main/accepted/0009-async.rst
- https://channels.readthedocs.io/en/latest/
- https://docs.python.org/3/library/asyncio.html
- https://www.djangoproject.com/weblog/2019/dec/02/django-30-released/
- https://www.noze.it/insights/django-async/
Immagine di copertina: Schermata del browser con la pagina di benvenuto predefinita di Django 3.0: razzo verde e messaggio “The install worked successfully!” — schermata di Ferbinu, CC BY-SA 4.0 — https://commons.wikimedia.org/wiki/File:Django_landing_page_3.0.png