Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Just Add Await: Retrofitting Async Into Django

Just Add Await: Retrofitting Async Into Django

A talk I gave at PyCon Australia 2019 (and then later gave a modified version of at DjangoCon US 2019)

Andrew Godwin

August 02, 2019
Tweet

More Decks by Andrew Godwin

Other Decks in Programming

Transcript

  1. Hi, I’m Andrew Godwin • Django contributor (Migrations/Channels) • Principal

    Engineer at • I see Python threads in my sleep now
  2. Django In Depth 1. 2. 3. Threading, cooperation, and intrigue

    Spanning two worlds with one vision Handlers, Requests, Middleware & Views Big Framework Problems Async In Brief
  3. You don't know when it'll switch! Threads are preemptive Coroutines

    are cooperative They only yield at an await.
  4. You don't know when it'll switch! Threads are preemptive Coroutines

    are cooperative They only yield at an await.
  5. An event loop runs in a single thread Yes, you

    can have threads and coroutines!
  6. # Call sync from sync result = function() # Call

    async from async result = await function()
  7. # Call async from sync in Python 3.7 result =

    asyncio.run(function()) # Python 3.6 loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) result = loop.run_until_complete(function())
  8. # Call sync from async executor = ThreadPoolExecutor(max_workers=3) loop =

    asyncio.get_event_loop() result = await loop.run_in_executor( executor, function, *args, )
  9. Async calling Sync is dangerous It has to be in

    a separate thread or it'll block the event loop
  10. # You can have this... result = cache.get("my-key") # Or

    this. Not both. result = await cache.get("my-key")
  11. # While this can have an async version... instance =

    await Model.objects.get() # There's no async way of doing this print(instance.foreign_key.name)
  12. Async has to add, not replace Sync Django is still

    important Things need to look familiar We don't want a wildly different API feel Things need to be safe Deadlocking or blocking is easier than ever
  13. Handler ASGI / WSGI Server Middleware View ORM Template URL

    Router Forms Phase One Phase Two Phase Three
  14. Phase One: ASGI Support Allowing Django to be async at

    all Phase Two: Async Views Unlocking async use in normal apps Phase Three: The ORM High-level async use for the most common case
  15. Phase One: Django 3.0 It's already committed! Phase Two: Django

    3.1 Unless things really do turn out nicely... Phase Three: Django 3.2/4.0 There's a lot of work here.
  16. “ James Bennett, "Django and NIH", 2006 Just so you

    know, Django is a smug, arrogant framework that doesn’t play nice with others.
  17. “ James Bennett, "Django and NIH", 2006 Just so you

    know, Django is a smug, arrogant framework that doesn’t play nice with others. [...] Or at least, that’s the impression you’d get from reading the rants...
  18. Custom request/response objects Most other frameworks did this too Custom

    "handler" classes Abstracts away WSGI Custom middleware Wow, was this contentious at the time!
  19. WSGIHandler __call__ WSGI Server WSGIRequest BaseHandler get_response URLs Middleware View

    __call__ HTTP protocol Socket handling Transfer encodings Headers-to-META Upload file wrapping GET/POST parsing Exception catching Atomic view wrapper
  20. WSGIHandler __call__ WSGI Server WSGIRequest BaseHandler get_response URLs Middleware View

    __call__ ASGIHandler __call__ ASGI Server ASGIRequest Asynchronous
  21. body_file = tempfile.SpooledTemporaryFile(max_size=..., mode='w+b') while True: message = await receive()

    if message['type'] == 'http.disconnect': # Early client disconnect. raise RequestAborted() # Add a body chunk from the message, if provided. if 'body' in message: body_file.write(message['body']) # Quit out if that's the end. if not message.get('more_body', False): break body_file.seek(0) return body_file
  22. if response.streaming: # Access `__iter__` and not `streaming_content` directly in

    case # it has been overridden in a subclass. for part in response: for chunk, _ in self.chunk_bytes(part): await send({ 'type': 'http.response.body', 'body': chunk, # Ignore "more" as there may be more parts; instead, # use an empty final closing message with False. 'more_body': True, }) # Final closing message. await send({'type': 'http.response.body'})
  23. WSGIHandler __call__ WSGI Server WSGIRequest BaseHandler get_response URLs Middleware View

    __call__ ASGIHandler __call__ ASGI Server ASGIRequest Asynchronous
  24. Propagates exceptions nicely Really helps with debugging! Proxies threadlocals down

    correctly Because people really love threadlocals. Stickies sync code into one thread We'll get back to this. It's nasty.
  25. WSGIHandler __call__ WSGI Server WSGIRequest BaseHandler get_response URLs Middleware View

    __call__ ASGIHandler __call__ ASGI Server ASGIRequest Asynchronous
  26. WSGIHandler __call__ WSGI Server WSGIRequest BaseHandler get_response URLs Middleware Async

    View __call__ ASGIHandler __call__ ASGI Server ASGIRequest Asynchronous Sync View __call__
  27. There's a whole talk in how this works! Also, it's

    not pretty or nice and it really shouldn't be necessary.
  28. Transactions Views are auto-wrapped in them with ATOMIC_REQUESTS Templates Direct

    calls from error handlers Tracebacks They're really long with all the switch functions
  29. # Iteration is the one transparent thing for result in

    Model.objects.filter(name="Andrew"): >>> QuerySet.__iter__ # This can work in the same codebase! async for result in Model.objects.filter(name="Andrew"): >>> QuerySet.__aiter__
  30. # But some things will never work - # we'll

    need to force select_related result = instance.foreign_key.name
  31. aeracode.org/2018/02/19/python-async-simplified/ A deeper dive into async vs. sync functions github.com/django/deps/blob/master/accepted/0009-async.rst

    DEP 0009, the proposal for async in Django code.djangoproject.com/wiki/AsyncProject Where to go to help