From 421622548060499881df9966b7a352bce63901cd Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Thu, 26 Mar 2020 15:46:24 +0000 Subject: [PATCH] Clarified async documentation. --- docs/spelling_wordlist | 1 - docs/topics/async.txt | 203 +++++++++++++++++--------------- docs/topics/http/middleware.txt | 11 +- docs/topics/http/views.txt | 19 +-- docs/topics/testing/tools.txt | 2 +- 5 files changed, 124 insertions(+), 112 deletions(-) diff --git a/docs/spelling_wordlist b/docs/spelling_wordlist index 3d2d214409..99bdba17b3 100644 --- a/docs/spelling_wordlist +++ b/docs/spelling_wordlist @@ -144,7 +144,6 @@ databrowse datafile dataset datasets -datastores datatype datetimes Debian diff --git a/docs/topics/async.txt b/docs/topics/async.txt index 2ca76c972b..3b0e72be3e 100644 --- a/docs/topics/async.txt +++ b/docs/topics/async.txt @@ -8,15 +8,15 @@ Asynchronous support Django has support for writing asynchronous ("async") views, along with an entirely async-enabled request stack if you are running under -:doc:`ASGI ` rather than WSGI. Async views will -still work under WSGI, but with performance penalties, and without the ability -to have efficient long-running requests. +:doc:`ASGI `. Async views will still work under +WSGI, but with performance penalties, and without the ability to have efficient +long-running requests. -We're still working on asynchronous support for the ORM and other parts of -Django; you can expect to see these in future releases. For now, you can use -the :func:`sync_to_async` adapter to interact with normal Django, as well as -use a whole range of Python asyncio libraries natively. See below for more -details. +We're still working on async support for the ORM and other parts of Django. +You can expect to see this in future releases. For now, you can use the +:func:`sync_to_async` adapter to interact with the sync parts of Django. +There is also a whole range of async-native Python libraries that you can +integrate with. .. versionchanged:: 3.1 @@ -40,20 +40,21 @@ class-based view, this means making its ``__call__()`` method an ``async def`` coroutine, ensure you set the ``_is_coroutine`` attribute of the view to ``asyncio.coroutines._is_coroutine`` so this function returns ``True``. -Under a WSGI server, asynchronous views will run in their own, one-off event -loop. This means that you can do things like parallel, async HTTP calls to APIs -without any issues, but you will not get the benefits of an asynchronous -request stack. +Under a WSGI server, async views will run in their own, one-off event loop. +This means you can use async features, like parallel async HTTP requests, +without any issues, but you will not get the benefits of an async stack. -If you want these benefits - which are mostly around the ability to service -hundreds of connections without using any Python threads (enabling slow -streaming, long-polling, and other exciting response types) - you will need to -deploy Django using :doc:`ASGI ` instead. +The main benefits are the ability to service hundreds of connections without +using Python threads. This allows you to use slow streaming, long-polling, and +other exciting response types. + +If you want to use these, you will need to deploy Django using +:doc:`ASGI ` instead. .. warning:: You will only get the benefits of a fully-asynchronous request stack if you - have *no synchronous middleware* loaded into your site; if there is a piece + have *no synchronous middleware* loaded into your site. If there is a piece of synchronous middleware, then Django must use a thread per request to safely emulate a synchronous environment for it. @@ -63,22 +64,30 @@ deploy Django using :doc:`ASGI ` instead. on debug logging for the ``django.request`` logger and look for log messages about *`"Synchronous middleware ... adapted"*. -In either ASGI or WSGI mode, though, you can safely use asynchronous support to -run code in parallel rather than serially, which is especially handy when -dealing with external APIs or datastores. +In both ASGI and WSGI mode, you can still safely use asynchronous support to +run code in parallel rather than serially. This is especially handy when +dealing with external APIs or data stores. -If you want to call a part of Django that is still synchronous (like the ORM) -you will need to wrap it in a :func:`sync_to_async` call, like this:: +If you want to call a part of Django that is still synchronous, like the ORM, +you will need to wrap it in a :func:`sync_to_async` call. For example:: from asgiref.sync import sync_to_async - results = sync_to_async(MyModel.objects.get)(pk=123) + results = sync_to_async(Blog.objects.get)(pk=123) You may find it easier to move any ORM code into its own function and call that -entire function using :func:`sync_to_async`. If you accidentally try to call -part of Django that is still synchronous-only from an async view, you will -trigger Django's :ref:`asynchronous safety protection ` to -protect your data from corruption. +entire function using :func:`sync_to_async`. For example:: + + from asgiref.sync import sync_to_async + + @sync_to_async + def get_blog(pk): + return Blog.objects.select_related('author').get(pk=pk) + +If you accidentally try to call a part of Django that is still synchronous-only +from an async view, you will trigger Django's +:ref:`asynchronous safety protection ` to protect your data from +corruption. Performance ----------- @@ -88,56 +97,56 @@ WSGI, or a traditional sync view under ASGI), Django must emulate the other call style to allow your code to run. This context-switch causes a small performance penalty of around a millisecond. -This is true of middleware as well, however. Django will attempt to minimize -the number of context-switches. If you have an ASGI server, but all your -middleware and views are synchronous, it will switch just once, before it +This is also true of middleware. Django will attempt to minimize the number of +context-switches between sync and async. If you have an ASGI server, but all +your middleware and views are synchronous, it will switch just once, before it enters the middleware stack. -If, however, you put synchronous middleware between an ASGI server and an +However, if you put synchronous middleware between an ASGI server and an asynchronous view, it will have to switch into sync mode for the middleware and -then back to asynchronous mode for the view, holding the synchronous thread -open for middleware exception propagation. This may not be noticeable, but bear -in mind that even adding a single piece of synchronous middleware can drag your -whole async project down to running with one thread per request, and the -associated performance penalties. +then back to async mode for the view. Django will also hold the sync thread +open for middleware exception propagation. This may not be noticeable at first, +but adding this penalty of one thread per request can remove any async +performance advantage. -You should do your own performance testing to see what effect ASGI vs. WSGI has -on your code. In some cases, there may be a performance increase even for -purely-synchronous codebase under ASGI because the request-handling code is -still all running asynchronously. In general, though, you will only want to -enable ASGI mode if you have asynchronous code in your site. +You should do your own performance testing to see what effect ASGI versus WSGI +has on your code. In some cases, there may be a performance increase even for +a purely synchronous codebase under ASGI because the request-handling code is +still all running asynchronously. In general you will only want to enable ASGI +mode if you have asynchronous code in your project. .. _async-safety: -Async-safety +Async safety ============ -Certain key parts of Django are not able to operate safely in an asynchronous +Certain key parts of Django are not able to operate safely in an async environment, as they have global state that is not coroutine-aware. These parts of Django are classified as "async-unsafe", and are protected from execution in -an asynchronous environment. The ORM is the main example, but there are other -parts that are also protected in this way. +an async environment. The ORM is the main example, but there are other parts +that are also protected in this way. If you try to run any of these parts from a thread where there is a *running event loop*, you will get a :exc:`~django.core.exceptions.SynchronousOnlyOperation` error. Note that you don't have to be inside an async function directly to have this error occur. If -you have called a synchronous function directly from an asynchronous function -without going through something like :func:`sync_to_async` or a threadpool, -then it can also occur, as your code is still running in an asynchronous -context. +you have called a sync function directly from an async function, +without using :func:`sync_to_async` or similar, then it can also occur. This is +because your code is still running in a thread with an active event loop, even +though it may not be declared as async code. If you encounter this error, you should fix your code to not call the offending -code from an async context; instead, write your code that talks to async-unsafe -in its own, synchronous function, and call that using -:func:`asgiref.sync.sync_to_async`, or any other preferred way of running -synchronous code in its own thread. +code from an async context. Instead, write your code that talks to async-unsafe +functions in its own, sync function, and call that using +:func:`asgiref.sync.sync_to_async` (or any other way of running sync code in +its own thread). -If you are *absolutely* in dire need to run this code from an asynchronous -context - for example, it is being forced on you by an external environment, -and you are sure there is no chance of it being run concurrently (e.g. you are -in a Jupyter_ notebook), then you can disable the warning with the -``DJANGO_ALLOW_ASYNC_UNSAFE`` environment variable. +You may still be forced to run sync code from an async context. For example, +if the requirement is forced on you by an external environment, such as in a +Jupyter_ notebook. If you are sure there is no chance of the code being run +concurrently, and you *absolutely* need to run this sync code from an async +context, then you can disable the warning by setting the +``DJANGO_ALLOW_ASYNC_UNSAFE`` environment variable to any value. .. warning:: @@ -147,6 +156,8 @@ in a Jupyter_ notebook), then you can disable the warning with the If you need to do this from within Python, do that with ``os.environ``:: + import os + os.environ["DJANGO_ALLOW_ASYNC_UNSAFE"] = "true" .. _Jupyter: https://jupyter.org/ @@ -154,11 +165,11 @@ If you need to do this from within Python, do that with ``os.environ``:: Async adapter functions ======================= -It is necessary to adapt the calling style when calling synchronous code from -an asynchronous context, or vice-versa. For this there are two adapter -functions, made available from the ``asgiref.sync`` package: -:func:`async_to_sync` and :func:`sync_to_async`. They are used to transition -between sync and async calling styles while preserving compatibility. +It is necessary to adapt the calling style when calling sync code from an async +context, or vice-versa. For this there are two adapter functions, from the +``asgiref.sync`` module: :func:`async_to_sync` and :func:`sync_to_async`. They +are used to transition between the calling styles while preserving +compatibility. These adapter functions are widely used in Django. The `asgiref`_ package itself is part of the Django project, and it is automatically installed as a @@ -171,28 +182,31 @@ dependency when you install Django with ``pip``. .. function:: async_to_sync(async_function, force_new_loop=False) -Wraps an asynchronous function and returns a synchronous function in its place. -Can be used as either a direct wrapper or a decorator:: +Takes an async function and returns a sync function that wraps it. Can be used +as either a direct wrapper or a decorator:: from asgiref.sync import async_to_sync - sync_function = async_to_sync(async_function) - - @async_to_sync - async def async_function(...): + async def get_data(...): ... -The asynchronous function is run in the event loop for the current thread, if -one is present. If there is no current event loop, a new event loop is spun up -specifically for the async function and shut down again once it completes. In -either situation, the async function will execute on a different thread to the -calling code. + sync_get_data = async_to_sync(get_data) + + @async_to_sync + async def get_other_data(...): + ... + +The async function is run in the event loop for the current thread, if one is +present. If there is no current event loop, a new event loop is spun up +specifically for the single async invocation and shut down again once it +completes. In either situation, the async function will execute on a different +thread to the calling code. Threadlocals and contextvars values are preserved across the boundary in both directions. :func:`async_to_sync` is essentially a more powerful version of the -:py:func:`asyncio.run` function available in Python's standard library. As well +:py:func:`asyncio.run` function in Python's standard library. As well as ensuring threadlocals work, it also enables the ``thread_sensitive`` mode of :func:`sync_to_async` when that wrapper is used below it. @@ -201,8 +215,8 @@ as ensuring threadlocals work, it also enables the ``thread_sensitive`` mode of .. function:: sync_to_async(sync_function, thread_sensitive=False) -Wraps a synchronous function and returns an asynchronous (awaitable) function -in its place. Can be used as either a direct wrapper or a decorator:: +Takes a sync function and returns an async function that wraps it. Can be used +as either a direct wrapper or a decorator:: from asgiref.sync import sync_to_async @@ -220,33 +234,32 @@ in its place. Can be used as either a direct wrapper or a decorator:: Threadlocals and contextvars values are preserved across the boundary in both directions. -Synchronous functions tend to be written assuming they all run in the main +Sync functions tend to be written assuming they all run in the main thread, so :func:`sync_to_async` has two threading modes: -* ``thread_sensitive=False`` (the default): the synchronous function will run - in a brand new thread which is then closed once it completes. +* ``thread_sensitive=False`` (the default): the sync function will run in a + brand new thread which is then closed once the invocation completes. -* ``thread_sensitive=True``: the synchronous function will run in the same - thread as all other ``thread_sensitive`` functions, and this will be the main - thread, if the main thread is synchronous and you are using the - :func:`async_to_sync` wrapper. +* ``thread_sensitive=True``: the sync function will run in the same thread as + all other ``thread_sensitive`` functions. This will be the main thread, if + the main thread is synchronous and you are using the :func:`async_to_sync` + wrapper. Thread-sensitive mode is quite special, and does a lot of work to run all functions in the same thread. Note, though, that it *relies on usage of* :func:`async_to_sync` *above it in the stack* to correctly run things on the -main thread. If you use ``asyncio.run()`` (or other options instead), it will -fall back to just running thread-sensitive functions in a single, shared thread -(but not the main thread). +main thread. If you use ``asyncio.run()`` or similar, it will fall back to +running thread-sensitive functions in a single, shared thread, but this will +not be the main thread. The reason this is needed in Django is that many libraries, specifically database adapters, require that they are accessed in the same thread that they -were created in, and a lot of existing Django code assumes it all runs in the -same thread (e.g. middleware adding things to a request for later use by a -view). +were created in. Also a lot of existing Django code assumes it all runs in the +same thread, e.g. middleware adding things to a request for later use in views. Rather than introduce potential compatibility issues with this code, we instead -opted to add this mode so that all existing Django synchronous code runs in the -same thread and thus is fully compatible with asynchronous mode. Note, that -synchronous code will always be in a *different* thread to any async code that -is calling it, so you should avoid passing raw database handles or other -thread-sensitive references around in any new code you write. +opted to add this mode so that all existing Django sync code runs in the same +thread and thus is fully compatible with async mode. Note that sync code will +always be in a *different* thread to any async code that is calling it, so you +should avoid passing raw database handles or other thread-sensitive references +around. diff --git a/docs/topics/http/middleware.txt b/docs/topics/http/middleware.txt index 3fe00b947f..1626d16992 100644 --- a/docs/topics/http/middleware.txt +++ b/docs/topics/http/middleware.txt @@ -308,10 +308,10 @@ on your middleware factory function or class: asynchronous requests. Defaults to ``False``. If your middleware has both ``sync_capable = True`` and -``async_capable = True``, then Django will pass it the request in whatever form -it is currently in. You can work out what type of request you have by seeing -if the ``get_response`` object you are passed is a coroutine function or not -(using :py:func:`asyncio.iscoroutinefunction`). +``async_capable = True``, then Django will pass it the request without +converting it. In this case, you can work out if your middleware will receive +async requests by checking if the ``get_response`` object you are passed is a +coroutine function, using :py:func:`asyncio.iscoroutinefunction`. The ``django.utils.decorators`` module contains :func:`~django.utils.decorators.sync_only_middleware`, @@ -328,8 +328,7 @@ methods, if they are provided, should also be adapted to match the sync/async mode. However, Django will individually adapt them as required if you do not, at an additional performance penalty. -Here's an example of how to detect and adapt your middleware if it supports -both:: +Here's an example of how to create a middleware function that supports both:: import asyncio from django.utils.decorators import sync_and_async_middleware diff --git a/docs/topics/http/views.txt b/docs/topics/http/views.txt index 1d3f9b5a11..554e93386c 100644 --- a/docs/topics/http/views.txt +++ b/docs/topics/http/views.txt @@ -205,25 +205,26 @@ in a test view. For example:: .. _async-views: -Asynchronous views -================== +Async views +=========== .. versionadded:: 3.1 As well as being synchronous functions, views can also be asynchronous -functions (``async def``). Django will automatically detect these and run them -in an asynchronous context. You will need to be using an asynchronous (ASGI) -server to get the full power of them, however. +("async") functions, normally defined using Python's ``async def`` syntax. +Django will automatically detect these and run them in an async context. +However, you will need to use an async server based on ASGI to get their +performance benefits. -Here's an example of an asynchronous view:: +Here's an example of an async view:: - from django.http import HttpResponse import datetime + from django.http import HttpResponse async def current_datetime(request): now = datetime.datetime.now() html = 'It is now %s.' % now return HttpResponse(html) -You can read more about Django's asynchronous support, and how to best use -asynchronous views, in :doc:`/topics/async`. +You can read more about Django's async support, and how to best use async +views, in :doc:`/topics/async`. diff --git a/docs/topics/testing/tools.txt b/docs/topics/testing/tools.txt index 64fee656bb..992289c0a9 100644 --- a/docs/topics/testing/tools.txt +++ b/docs/topics/testing/tools.txt @@ -1808,7 +1808,7 @@ creates. @mock.patch(...) @async_to_sync - def test_my_thing(self): + async def test_my_thing(self): ... .. _topics-testing-email: