#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 django-updates+unsubscr...@googlegroups.com.
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.

Reply via email to