#33735: Add asynchronous responses for use with an ASGI server
--------------------------------+------------------------------------------
Reporter: florianvazelle | Owner: florianvazelle
Type: New feature | Status: assigned
Component: HTTP handling | Version: 4.0
Severity: Normal | Resolution:
Keywords: ASGI async | Triage Stage: Accepted
Has patch: 1 | Needs documentation: 0
Needs tests: 0 | Patch needs improvement: 1
Easy pickings: 0 | UI/UX: 0
--------------------------------+------------------------------------------
Changes (by Carlton Gibson):
* cc: Andrew Godwin, Michael Brown (added)
Comment:
Hi Florian — Thanks again for this.
I **finally** got the time to sit with the PR and that for #32798 to try
and think through the right way forward.
I'm commenting here, rather than the PR, because I think we need a
slightly different approach.
Also cc-ing Andrew and Micheal to ask for their input too.
So what follows is an RFC...
Looking at the [https://github.com/django/django/pull/15727 PR here], and
that for #32798 (either the [https://github.com/django/django/pull/14526
complex first suggestion], or the
[https://github.com/django/django/pull/14652 simpler but slower
alternative]) I think we should consciously opt-out of trying to map
between sync and async generators. **Maybe** somebody writes a queue
implementation that has sync one end and async at the other, and handles
thread-safety, and back-pressure, and who knows what. **Maybe** we could
use that in Django if we felt it trust-worthy enough. But I don't think we
should try and write one ourselves.
Rather, I think we should say that, in the case of streaming responses,
users need to adjust their code for whether they're planning to run under
WSGI or ASGI. (We will handle the **other** case both ways, but at a
performance and memory cost.)
To wit:
We add `__aiter__` to `StreamingHttpResponse` and adopt `ASGIHandler` to
use `async for` in the `if response.streaming` block. Under ASGI you would
provide `StreamingHttpResponse` with an async iterator, still yielding
bytestrings as content. This should allow the streaming async responses
use-case.
Under WSGI you'd continue to provide a sync iterator and everything would
work as it is already.
For each case, `__iter__` and `__aiter__` we handle the **wrong case** by
consuming the iterator in a single step (using the `asgiref.sync`
utilities, with `thread_sensitive` for `sync_to_async`) and then yield it
out in parts as expected. (This is the performance and memory cost.) We
log a **warning** (that may be filtered in a logging content if not
desired) saying that folks should use an appropriate generator type in
this case.
Note that this is similar to the `__aiter__` implementation on QuerySet,
which
[https://github.com/django/django/blob/f30c7e381c94f41d361877d8a3e90f8cfb391709/django/db/models/query.py#L397-L405
fetches all the records once inside a single sync_to_async()] to avoid
multiple (costly) context switches there:
{{{
def __aiter__(self):
# Remember, __aiter__ itself is synchronous, it's the thing it
returns
# that is async!
async def generator():
await sync_to_async(self._fetch_all)()
for item in self._result_cache:
yield item
return generator()
}}}
I think this should be both good enough and maintainable. In the happy
case, where you provide the right kind of iterator for your chosen
protocol (ASGI/WSGI) you get the result you want, without us need anything
overly complex. In the deviant case it'll work (assuming you're not trying
to stream ''Too much™''). It means we're not branching in the handlers to
handle each of the different possible iterator types. The abstraction
leaks a little bit, but I kind of think that's reasonable at this point.
Maybe: If it proves impossible to get the WSGI+async/ASGI+sync fallback to
work correctly, just erroring at that point would maybe be acceptable. 🤔
(That you could provide an async iterator to ASGIHandler would allow long-
lived responses, that aren't currently feasible.)
Thoughts? Thanks!
**Assuming** all that makes sense, would you (or Michael maybe) want to
take this on?
If not I can pick it up, since it would be good to hit Django 4.2 (feature
freeze Jan 23) for this. 🎯
--
Ticket URL: <https://code.djangoproject.com/ticket/33735#comment:8>
Django <https://code.djangoproject.com/>
The Web framework for perfectionists with deadlines.
--
You received this message because you are subscribed to the Google Groups
"Django updates" group.
To unsubscribe from this group and stop receiving emails from it, send an email
to [email protected].
To view this discussion on the web visit
https://groups.google.com/d/msgid/django-updates/01070183c74d3e48-26527c6d-aa7c-4f96-9f8d-e0bc79970311-000000%40eu-central-1.amazonses.com.